Orchestrator framework + first-principles improvement cycle (D009–D013) - #2
Orchestrator framework + first-principles improvement cycle (D009–D013)#2JosephOIbrahim wants to merge 19 commits into
Conversation
…xt session entry point PR #1 merged to main at e08aebb (merge commit; method: merge — preserves the 18-commit lane-by-lane history). Branch claude/hanna-mcp-review-ZsorY consumed against main. NEXT.md refreshed end-to-end as the post-merge handoff: - Top section: merge boundary marker (PR # + merge commit SHA + method note). - Lane commits list: all 11 contributing commits in HEAD order (8 from the lane work + 3 from CodeRabbit + diagnostic batches), not just the 7 lanes covered by the prior 2e68877 snapshot. - Test count: 74 (was 73; round-3 added one ISO-datetime test). - Substrate-decision tree: D005 + D007 rows now note the round-3 patches (frame coalescing for D005 in 298f50c; ISO-datetime parse for D007 in 298f50c). - "Where you are": branch consumed; PR merged; main HEAD = e08aebb; CodeRabbit review state (13/13 threads addressed across 3 rounds). - "Next session entry point — L4b": unchanged (L4b dispatch shape pre-drafted; carries over). - "Open questions still parked": §C.3 now explicitly closed by L3b + round-3 frame-coalescing patch. - New section "Lessons from this session" — three patterns surfaced by this session that future buildout sessions should anticipate: invocation > mocked tests for edge bugs; deeper review (CodeRabbit round 3) > self-audit confidence; doc-drift accumulates faster than code-drift (diagnostic survey catches what CodeRabbit's grep misses). - Staleness flag (Session 01 §G) carried forward unchanged. This commit lands on claude/hanna-mcp-review-ZsorY (the merged branch) so it remains as the source-of-truth handoff for the next session. The branch is now 1 commit ahead of main; that's the NEXT.md post-merge update, intentional, and can be cherry-picked to main in a follow-up if desired. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
…a state (D009)
Stands up a generalized plan/delegate/verify/ship orchestrator as the
umbrella operating mode for multi-agent work in this repo. Ratified by
Joe this session via two decisions (D009.1 generalize&reuse; D009.2
separate ORCHESTRATOR.md).
Design principle — generalize & reuse, no duplication. An inventory
found Hanna already implements ~70% of the framework: D002 MoE roles
map 1:1 to planner/worker/critic/integrator; DECISIONS/ROADMAP/NEXT
map to decisions/plan/checkpoint. The generic state names are aliases
for those existing files (documented in ORCHESTRATOR.md §7); only the
two genuinely-new artifacts are created.
Files created (7):
- ORCHESTRATOR.md — the operating manual verbatim (§1 Roles, §2
Delegation Contract, §3 State, §4 Termination, §5 Context Budget,
§6 Invariants) + three Hanna-adapter sections: §7 State Adapter
(the alias mapping + checkpoint field crosswalk; no duplicate
standing files), §8 Relationship to /hanna-dispatch-next (the slash
command is one workflow under the orchestrator — it expresses
planner->worker(s)->critic[verify]->integrator->commit per lane),
§9 hard-constraint inheritance (Rules 34/35/36/37, canonical trailer,
D003/D004 hygiene, no-deps-without-a-decision).
- state/beliefs.md — durable, single-writer belief layer (9-column
supersession-tracked table). Seeded with 5 claims. c003 deliberately
carries 0.7 (below the 0.8 threshold): "a worker's done warrants only
moderate confidence until critic[verify] confirms" — the encoded
lesson of the 2026-05-22 session, where CodeRabbit round-3 found 4
latent bugs after this thread self-declared L3b/L4a done.
- state/open_questions.md — leverage-ranked q-IDs. Seeded with 3 from
NEXT.md parked items (q002 LockoutResponse = high, gates L6; q001
Octavius envelope = med; q003 SESSION_01 §G staleness = low). §C.3
(closed by L3b) and §C.6 (voided by D008.1) intentionally not seeded.
- .claude/agents/{planner,worker,critic,integrator}.md — the D002
roles formalized as Claude Code subagent primitives. YAML frontmatter
(name/description/tools); planner+critic read-only, worker+integrator
read-write. critic defines all three modes (verify/red_team/evaluate).
Each carries the inherited hard constraints and the single-writer
belief rule (propose deltas in summary; never write beliefs.md).
Files edited (2):
- CLAUDE.md — Orchestration pointer block after "Read edges"; one
"not a duplicate of ORCHESTRATOR.md" line under "What this file is
not". Existing sections untouched.
- docs/DECISIONS.md — D009 entry (the substrate-level ratification of
the adoption, with rejected alternatives: replace&migrate, coexist,
merge-into-CLAUDE, global ~/.claude). Footer bumped to D010. This is
both Hanna's canonical ratification mechanism and the decisions.md
the orchestrator itself requires — the adoption is self-consistent.
Verified:
- ls -> all 7 files present; state/ holds exactly beliefs.md +
open_questions.md (no forbidden decisions.md/plan.md/checkpoint.md/
parked.md duplicates).
- All 4 agent files open with --- YAML frontmatter; critic.md has 3
MODE headers.
- beliefs.md c003 confidence = 0.7 (< 0.8 threshold, as intended).
- python3 -m pytest tests/ -q -> 74 passed (no code/test files
touched; pure docs + scaffold).
- grep "Cloned from Harlo|claude-opus" across new files -> 0 (all
fresh Hanna authoring, no Harlo ancestor per D003/D004 Clause B;
no model-id in any artifact).
Out of scope (deferred): running a live GOAL (first = L4b, separate
invocation); token-tracking field in NEXT.md (run-time concern); any
change to /hanna-dispatch-next internals (referenced, not modified).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <[email protected]>
…uts in flight Orchestrator hygiene per ORCHESTRATOR.md Invariants 1-2 (plan before execute; state on disk). This commit lands the GOAL scaffold for the in-flight scout phase; no scout findings yet (scouts still running). NOT implementation — Joe gates implementation on "go" after the scout phase completes and the PRD is presented. state/plan.md: GOAL = review Hanna from first principles; produce a PRD identifying opportunities + areas for improvement; surface to Joe for "go" before implementation. EXIT_CRITERIA: 7 scout findings delivered, PRD authored, beliefs/open_questions updated, Joe presented with explicit ask, implementation BLOCKED until "go". Task graph: 7 parallel critic[red_team] scouts (architecture, code-quality, tests, docs, ops, security-rules, lanes-schemas), orchestrator synthesis, report, wait. CONFIDENCE_THRESHOLD 0.8. state/checkpoint.md: scout phase in-flight; 7 task IDs registered; per-criterion EXIT_STATUS pending; budget tracking; phase = SCOUT. Out of scope for this commit: any scout findings (not landed yet), any belief-delta writes (orchestrator writes after scouts return), any PRD content (synthesized after scouts return), any implementation actions whatsoever (gated on Joe's "go"). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
…dings (2/7) Orchestrator hygiene per ORCHESTRATOR.md Invariant 2 (state on disk). Scout phase still in flight; 5 scouts remain in-flight; no synthesis yet (waits for all 7); no implementation actions taken (gated on Joe's "go" after PRD presentation). scout-architecture: 0 BLOCKER, 4 MAJOR. Key catch — the live GOAL (this run) violates ORCHESTRATOR.md §7's "only beliefs.md + open_questions.md are standing files in state/" rule by carrying state/plan.md as a standing file. (The GOAL block should live in the NEXT.md adapter slot.) Other MAJORs: D006 macOS coupling unresolved as L4b approaches; "brief composition" boundary undefined; DECISIONS.md template inconsistency on Rejected- alternatives blocks. scout-code-quality: 0 BLOCKER, 1 MAJOR. ProductFile.parse() has 6 silent-coercion/drop gaps (quoted YAML, duplicate keys, unknown section headers, empty bullet lines, asymmetric ISO-datetime fallback, no Unicode normalization) — violates Rule 36's faithful- surface posture because the composer silently drops mismatched products. Notably: confirms the round-3 frame-coalescing patch is correctly synchronized — _recv_buffer is only mutated inside self._lock-held call sites (no race introduced by the static-to- instance method conversion). Belief deltas (9 proposed across 2 scouts) will be validated and written to state/beliefs.md by the orchestrator AFTER all 7 scouts return. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
… (4/7) Orchestrator hygiene per Invariant 2. 3 scouts remain in-flight (scout-docs, scout-ops, scout-lanes-schemas). No synthesis yet (waits for all 7); no implementation actions taken (gated on Joe's "go" after PRD presentation). scout-security-rules: 2 BLOCKER, 3 MAJOR, 4 MINOR. - B-0: RULES.md compliance recipes target python/hanna/ + crates/, neither of which exists in the repo. The CI greps run against paths that don't exist — green light means nothing. - B-1: RULES.md asserts 3-layer lockout verification + the test file tests/test_integration/test_lockout.py that doesn't exist. Only layer 1 (compute_producer_phase) is tested; override_token is openly spec-only. - M-1: Rule 35 grep is name-anchored and trivially defeated by rename (harlo.commit/.persist would slip through); real defense is the enumerated bridge surface. - M-2: Rules 1-17/19-33 carry no per-rule "Not yet load-bearing" annotation despite D008.7 promising it. - M-3: SQLite write path uses default sqlite3.connect() — no journal_mode=WAL, no timeout, no PRAGMA hardening; will break on second-writer race. - Confirms lock discipline on production _rpc path is sound; _recv_buffer only mutated under self._lock; no race introduced by the round-3 static-to-instance method conversion. scout-tests: 3 BLOCKER, 9 MAJOR, 12 MINOR. - B1: zero regression test for the round-3 frame-coalescing patch (grep "recv_buffer" tests/ returns nothing despite the variable being wired through 3 sites in src/harlo_bridge.py). - B2: scripts/first_hanna_brief.py is 75% untested — only _portfolio_line has coverage. _state_line, _compose_brief, _persist, _extract_burnout, _read_harlo, _read_product_files, main() (incl. FAMILY_LOCKOUT early exit) have zero coverage; no integration test stitches them end-to-end. - B3: compute_producer_phase has 21 tests but misses its own MONTHLY-beats-WEEKLY precedence collisions (Jun 1 2026 returns MONTHLY, not WEEKLY_MONDAY — empirically verified, undocumented, untested) and exercises zero non-default keyword arguments. - Suggests raising belief c003 confidence 0.7 -> 0.8: empirically confirmed since the specific round-3 fix the c003 caution predicts has no regression test. Combined: 5 BLOCKERs across these 2 scouts. PRD will need to prioritize these for any "go" decision. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
…6/7)
Orchestrator hygiene per Invariant 2. 1 scout remains in-flight
(scout-ops). No synthesis yet; no implementation actions (gated on
Joe's "go").
scout-docs: 3 BLOCKER, 8 MAJOR, 6 MINOR.
- B1: RULES.md §34 still describes 3-layer lockout with HdProducer
as layer 2, contradicting RULES.md's own line-15 applicability
note (post-L1) and every other propagated surface (README,
BLUEPRINT, NEXT, UI_UX_MAP, ORCHESTRATOR §9).
- B2: README.md "Repository layout" is a one-third-true filesystem
map — omits ORCHESTRATOR.md, docs/DECISIONS.md, docs/ROADMAP.md,
docs/REVIEW_2026-05-22.md, docs/PRODUCER_LENS.md, docs/UI_UX_MAP.md,
docs/SPIKE_HARLO_EDGE_2026-05-20.md, state/, scripts/, src/,
tests/, data/; references nonexistent web/README.md; stamps
BLUEPRINT as v0.1.0-draft when file self-stamps v0.2.0-audit.
- B3: SESSION_01_RECON.md §G still claims "RULES.md does not exist
in Harlo, synthesize from distributed sources" — directly
contradicted by RULES.md:7 ("Extracted verbatim from Harlo/
CLAUDE.md lines 37-194"). The NEXT.md "staleness flag" carried
this across sessions without resolution.
- M-noteworthy: REVIEW_2026-05-22.md carries the model-id string
CLAUDE.md explicitly disallows. ORCHESTRATOR §7 adapter is
partly aspirational and missing state/plan.md — independently
cross-verifies scout-architecture's same finding.
scout-lanes-schemas: 3 BLOCKER, 8 MAJOR, 7 MINOR.
- B1: osascript path is macOS-only but CI runs ubuntu-latest;
lane needs HannaCalendarNotAvailable + platform gate. NEXT.md
names this exception, ROADMAP §4 does not (drift between
source docs).
- B2: rhythm-anchor contradiction. BriefPayload.composed_at_iso
is a UTC compose-moment timestamp by construction at
scripts/first_hanna_brief.py:138, but D006 promises rhythm-time
anchor events at e.g. 09:00 ET. ROADMAP §4 hardcodes the
compose-moment as the event start, so the first morning brief
would land at random compose time (e.g. 07:42 ET) defeating
D006's "context the day carries" posture rationale.
- B3: publish-now CLI subcommand cited by both ROADMAP §4 and
NEXT.md has zero specification — no argparse contract, no
source-of-brief, no error semantics.
- Forward-path observation: L4b is the only ship-able lane from
current HEAD. Reaching "Hanna is real" requires q004
(rhythm-anchor) resolved before L4b dispatch.
Cumulative across 6 scouts: 11 BLOCKER, 33 MAJOR, 33 MINOR.
PRD will need clear prioritization.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <[email protected]>
…iting Joe's "go"
Orchestrator first-principles review GOAL reached the synthesis-and-
report milestone. 7 critic[red_team] scouts dispatched, all returned;
9 belief deltas validated and written to state/beliefs.md (c006-c014);
10 open questions promoted to state/open_questions.md (q004-q013);
PRD authored at state/tasks/prd/PRD.md with prioritized recommendations.
No implementation taken — gated on Joe's "go" per the GOAL spec.
scout-ops (this commit completes the 7/7):
- 2 BLOCKER: (B-OPS-001) "always-on" claim unbacked — no scheduler/
cron/launchd; bin/hanna-brief.command opens a static HTML mockup
and does not invoke Python; (B-OPS-002) BriefPayload has no
idempotency token; briefs SQLite has AUTOINCREMENT id with no
UNIQUE — L4b publish() would produce duplicate Joe-visible
calendar events on every retry.
- 5 MAJOR: zero `import logging` in src/; HarloBridge silent state-
blind degradation; override_token spec-only; no health probe;
undefended SQLite durability/retention.
- 5 MINOR; 5 belief deltas; 3 open questions; trailer hygiene clean.
Cumulative across 7 scouts: 13 BLOCKER, 38 MAJOR, ~38 MINOR.
PRD §"Recommended first implementation cycle": Phase 0 (orchestrator
self-fix per c009/q013) + Phase 1 (pre-L4b unblockers — D010 rhythm-
anchor / D011 cross-platform / D012 idempotency + publish-now CLI
spec + round-3 frame-coalescing regression test per c007) + Phase 4
partial (highest-impact doc drift — RULES §34, README layout,
SESSION_01 §G). Estimated 6-8 agent dispatches out of 13 remaining
in Joe's 20-cap.
Notable cross-scout findings:
- c003 ("done warrants critic[verify]") empirically vindicated by
c007 (round-3 frame-coalescing patch has zero regression test
despite being the exact bug class c003 predicts).
- ORCHESTRATOR.md §7 self-violation (state/plan.md is a standing
file despite the §7 adapter rule) cross-verified by scout-
architecture AND scout-docs independently — strong signal.
- L3b round-3 patch is correctly synchronized (scout-code-quality
+ scout-security-rules cross-verify); the BUG was the missing
test, not the code.
Belief layer state: 14 active claims; 13 open questions across
3 leverage tiers. State on disk per ORCHESTRATOR.md Invariant 2.
The ask is in PRD §"The ask": go / no-go / redirect on the
recommended first cycle. No orchestrator action permitted before
Joe's confirmation.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <[email protected]>
ORCHESTRATOR.md §7 amended to distinguish at-rest state (durable cross-GOAL: beliefs.md + open_questions.md only) from during-GOAL runtime state (plan.md, checkpoint.md, tasks/<id>/, gate.md). Closes belief c009 (orchestrator self-violation cross-verified by scout-architecture + scout-docs); ratified as belief c015. Closes open question q013. This is Phase 0 of Joe's "go on everything" directive on the PRD's 6-phase implementation cycle. Subsequent phases continue in this session: Phase 1 (pre-L4b unblockers: D010/D011/D012 + idempotency code + frame-coalescing regression test), Phase 2 (test integrity), Phase 3 (CI/compliance integrity), Phase 4 (doc drift sweep), Phase 5 (operational substrate), Phase 6 (data hygiene). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
… gate + D012 idempotency + frame-coalescing regression test
PRD Phase 1 of 6. Three ratified D-entries + the implementation that
satisfies them + the missing regression test belief c003 predicted.
Phase 1 critic[verify] PASS on 20/20 acceptance criteria.
D010 (rhythm-anchor): BriefPayload gains phase_anchor_iso (ET-anchored
ISO 8601 per phase: MORNING=09:00 / MIDDAY=12:00 / EVENING=17:00 /
WEEKLY_MONDAY=09:30 / WEEKLY_FRIDAY=16:00 / MONTHLY=09:00 first
weekday). compute_phase_anchor_iso(phase, compose_date) helper. Closes
q004 → belief c016 (0.9; ET timezone handling verified for both EDT
and EST).
D011 (cross-platform): macOS-only L4b stance with explicit
HannaCalendarNotAvailable exception on non-macOS. Phase 1 lands the
lazy ImportError → local stub fallback in scripts/first_hanna_brief.py
and the try/except wrapper around the (placeholder) publish call.
Full publish() body lands with L4b. Closes q005 conditionally → belief
c018 (0.7; pending L4b end-to-end exercise).
D012 (idempotency): BriefPayload gains brief_id = SHA256[:16] of
(phase + anchor_date + sorted products). briefs SQLite schema gains
brief_id TEXT UNIQUE + phase_anchor_iso TEXT columns. _persist switches
to INSERT OR IGNORE — re-running same composition is idempotent on disk.
Closes q006 → belief c017 (0.92; double-_persist verified to yield
COUNT(*)=1; supersedes c012).
Frame-coalescing regression test (the missing test belief c007 named):
TestFrameCoalescing in tests/test_harlo_bridge.py with 3 cases —
two-frames-in-one-read both decoded; partial second frame buffered in
_recv_buffer between calls; _recv_buffer cleared on close(). Closes
c007 → belief c019 (0.95; supersedes c007 from active to superseded).
MoE dispatch per D002 + ORCHESTRATOR.md §1:
- 2 workers in parallel (phase1-idempotency + phase1-frame-coalescing-
test); each cold-read, self-contained briefs per Delegation Contract.
- 1 critic[verify] for Phase 1 acceptance audit: PASS 20/20.
- Decisions D010/D011/D012 author-by-main-thread per D002.
Verified:
- python3 -m pytest tests/ -q → 83 passed in 0.45s (was 74; +6
idempotency tests + 3 frame-coalescing tests).
- PYTHONPATH=. python3 scripts/first_hanna_brief.py → exits 0
("Hanna paused: FAMILY_LOCKOUT" — 2026-05-30 is Saturday, expected).
- Rule 35/36/37 clean; D003/D004 trailer hygiene preserved
(scripts/first_hanna_brief.py keeps Harlo trailer; new test code in
fresh-seed test files carry no trailer); no model-id in artifacts.
State updates (per ORCHESTRATOR.md Invariant 2):
- state/beliefs.md: c007 superseded by c019; c012 superseded by c017;
c009 superseded by c015 (from Phase 0); c015–c019 added.
- state/open_questions.md: q004 (c016), q005 (c018), q006 (c017),
q013 (c015 from Phase 0) all closed.
Budget posture: Phase 1 spent 3 agents (2 workers + 1 critic[verify])
of the 13-remaining cap. 10 agents remain for Phases 2, 3, 5, 6.
Next: Phase 2 (test integrity — integration test for PoC; sub-render
coverage; MONTHLY-beats-WEEKLY precedence test + D013).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <[email protected]>
…+ MONTHLY precedence (D013) PRD Phase 2 of 6. Closes belief c006 (PoC 75% untested) and c008 (MONTHLY precedence undocumented). Phase 2 critic[verify] PASS on 13/13 acceptance criteria. PoC integration (TestMainIntegration × 3): - test_main_state_blind_path_produces_brief_and_persists: HarloUnreachable branch; exit-0; stdout brief; SQLite row with harlo_reachable=0 + populated brief_id + phase_anchor_iso; D012 idempotency on re-invoke. - test_main_family_lockout_path_exits_cleanly: FAMILY_LOCKOUT early-exit; exit-0; lockout stdout; no SQLite row created. - test_main_harlo_timeout_routes_to_state_blind: HarloTimeout degrades to state-blind (not crash); exit-0; state-blind brief. All use tmp_path-scoped SQLite; never touch real data/hanna.sqlite. Sub-render coverage (18 cases across 4 classes): - TestStateLine (5): state-blind; reachable+non-RED; reachable+RED; fallback paths. Each asserts Rule 36 voice cleanliness via the _assert_no_directives() helper that scans for "you should/must", "I recommend", etc. - TestApproachingLine (5): no FFs; single FF; sort-by-date; cap-at-3 (the actual production heuristic — c022 ratifies this clarification over the earlier "within-5-days" framing); missing-product graceful. - TestBlockersLine (5): no blockers; single product; two products; multiple blockers on one; missing-product graceful. - TestComposeBrief (3): state-blind MORNING end-to-end (asserts D010 09:00 ET anchor + D012 16-char brief_id + correct referenced_products); FAMILY_LOCKOUT (empty anchor + brief_id); reachable-empty-payload MIDDAY fallback. MONTHLY-beats-WEEKLY precedence (TestPhasePrecedence × 3 + D013): - D013 ratifies the existing branch order MONTHLY > WEEKLY_MONDAY > WEEKLY_FRIDAY > daily phases as intentional precedence (preserves monthly cadence; first-Monday/Friday-of-month emits MONTHLY). Rejected alternatives: prev_phase tracking, merging slots, raise on collision (Rule 36 violation). Footer bumped to D014. - 3 regression tests: 2026-06-01 (Mon + day-1) → MONTHLY; 2026-05-01 (Fri + day-1) → MONTHLY; 2026-06-08 (non-day-1 Mon) → WEEKLY_MONDAY. MoE dispatch per D002 + ORCHESTRATOR.md §1: - 3 workers in parallel: phase2-poc-integration + phase2-sub-render- coverage + phase2-monthly-precedence. Each cold-read, self-contained briefs. Disjoint test classes (no merge conflicts). - 1 critic[verify] for Phase 2 acceptance audit: PASS 13/13. Verified: - python3 -m pytest tests/ -q → 107 passed (was 83; +24 tests: TestMainIntegration 3 + TestStateLine 5 + TestApproachingLine 5 + TestBlockersLine 5 + TestComposeBrief 3 + TestPhasePrecedence 3). - Rule 36 voice: _assert_no_directives operationalized as a real assertion (not a comment) — 14 uses across sub-render tests. - Trailer hygiene: test files (fresh seeds) carry no trailer; D013 preserves DECISIONS.md format; no model-id in artifacts. - No production code modified — tests + DECISIONS.md D013 only. State updates: - state/beliefs.md: c006 superseded by c020; c008 superseded by c021; c020/c021/c022 added (c022 ratifies the cap-at-3 heuristic clarity). Budget posture: Phase 2 spent 4 agents (3 workers + 1 critic) of the 10 remaining post-Phase-1. 6 agents remain for Phases 3, 5, 6. Next: Phase 3 (CI/compliance integrity — repoint RULES.md greps; tighten Rule 35; annotate non-load-bearing rules per D008.7). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
PRD Phase 3 of 6. Closes belief c013 (CI greps targeted nonexistent
python/hanna/ + crates/ paths; green light meaningless). Phase 3
critic[verify] PASS on 11/11 acceptance criteria.
RULES.md compliance recipes now target only existing paths (src/,
scripts/, tests/, docs/); no more "No such file or directory" from
the §"Compliance Checks" recipes. (Prior session had already
repointed these; Phase 3 verified.)
.github/workflows/ci.yml — three substantive changes:
1. Rule 35 name-anchored grep extended from {write,store,author,mutate}
to {write,store,author,mutate,commit,persist,save,update} (the four
bypass verbs scout-security-rules M-1 flagged). Adds matching
octavius. grep for symmetry.
2. NEW step "Rule 35 — enumerated bridge surface (authoritative
allowlist per D001)" — inline python3 block parses
`self._call_tool("X"` invocations in src/harlo_bridge.py and asserts
X ⊆ {status, coach, recall, query_past_experience, patterns}. Fails
CI on any non-allowlisted name AND fails if zero sites found
(catches a broken regex). This is the AUTHORITATIVE check; the
name-anchored grep is now the first line of defense, the allowlist
is the gate. Verbatim output today:
`Rule 35 allowlist OK: found tools ['coach', 'patterns',
'query_past_experience', 'recall', 'status']` — exactly D001.
3. Rule 37 grep extended from {src/, scripts/, tests/} to
{src/, scripts/, tests/, docs/}. NEW step "Rule 37 — no patent
topics in commit messages (last 50)" runs `git log -50 --format=%B`
with a documented exemption pipeline for rule-discussion language
(Rule 37, grep recipes, patent-topic literal, patent-section,
"no patent"). Requires `git fetch --deepen=50` first since
actions/checkout defaults to depth=1.
RULES.md — added the per-rule annotation
`*Not yet load-bearing per [D008.7](docs/DECISIONS.md) — applies on
the session that lands the constrained component.*` to each of
rules 7-17 and 19-33. Rules 1-6 already had it (prior session).
Rules 18, 34, 35, 36, 37 deliberately UNANNOTATED (load-bearing per
RULES.md line 15). Total: 32 annotations now present; fulfills
D008.7's selective-re-adoption promise.
MoE dispatch per D002:
- 1 worker (phase3-ci-rules-hardening; cold-read self-contained brief).
- 1 critic[verify] PASS 11/11.
Verified:
- python3 -m pytest tests/ -q → 107 passed (unchanged).
- YAML parse of .github/workflows/ci.yml → valid.
- Rule 35 allowlist check empirically returns D001's 5 tools, no extras.
- Rule 37 grep against docs/ + commit messages: clean (with exemptions).
- 32 D008.7 annotations counted; load-bearing rules unannotated.
- No model-id strings in modified files; no production code changes.
State updates:
- state/beliefs.md: c013 superseded by c023; c024 added (D008.7
selective-re-adoption complete).
Budget posture: Phase 3 spent 2 agents (1 worker + 1 critic) of the
6 remaining post-Phase-2. 4 agents remain for Phases 5 + 6 (Phase 4
is main-thread doc cleanup).
Next: Phase 4 (doc-drift sweep — RULES.md §34 to 2-layer, README
Repository layout regenerated, SESSION_01 §G resolved, REVIEW
2026-05-22 model-id removed). Main-thread; 0 agents.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <[email protected]>
…generated, SESSION_01 §G historical stamp (closes q003/q009/q010) PRD Phase 4 of 6. Main-thread doc cleanup; 0 agents. Phase 4 addresses the highest-impact drift contradictions surfaced by scout-docs that survive the prior reconciliations (2e68877 + 8e83296). Closes beliefs c003/c009/c010 conditional gaps + q003 + q009 + q010. RULES.md §34 (lines 211-219): rewrites "Enforced at three layers" to "Enforced at two active layers (Layer 2 was Cut per D008.1; layer numbering preserved for backward reference)". Layer 2 row strikethrough'd with a one-line note that the layer collapses into Layer 3. Layer 3 annotated as "Currently deferred to the L6 mcp_tools lane". Closing sentence updated: "Tests verify the two active layers; the layer-3 test scaffold lands with L6. Bypassing any active layer fails CI." Resolves the self-contradiction scout-docs B1 named (line 15 said 2 active; §34 still said 3). README.md "Repository layout" (lines ~159-188): regenerated against the actual filesystem. Now includes ORCHESTRATOR.md, state/ (with during-GOAL note per ORCHESTRATOR §7), src/ (schemas, harlo_bridge, computations), scripts/, tests/, .claude/ (agents + commands), .github/workflows/ci.yml, and the full docs/ listing (DECISIONS, ROADMAP, PRODUCER_LENS, UI_UX_MAP, REVIEW_2026-05-22, SPIKE_HARLO_EDGE, SESSION_01_RECON). BLUEPRINT version stamp corrected from v0.1.0-draft to v0.2.0-audit (matches the file's self-stamp). Nonexistent web/README.md reference dropped per scout-docs B2. SESSION_01_RECON.md §G: added a `> Historical note (2026-05-25, Phase 4 doc-drift sweep)` block at the top of §G explaining that the recon-time premise was wrong — the 33 rules existed verbatim in Harlo/CLAUDE.md lines 37–194 throughout Session 01, and Session 01.5 extracted them directly. The recon-time prose stays unedited below as a session-stamped historical artifact (per q003's default recommendation: stamp, don't rewrite). NEXT.md: staleness flag (lines 92-94) reframed from "carry forward" to "resolved 2026-05-25" pointing at the SESSION_01 §G historical note. REVIEW_2026-05-22.md:4: model-id string removed. "main-thread (Claude Opus 4.7, 1M context)" → "main-thread". CLAUDE.md prohibits model-id in committed artifacts; Phase 3 CI grep on docs/ now enforces this — scout-docs MAJOR closed. Verified: - python3 -m pytest tests/ -q → 107 passed (no test files touched). - grep -rE "claude-opus|Claude Opus 4\." across docs/ src/ scripts/ tests/ + all root .md → 0 hits (q010 default closed by c026). - grep -c "Not yet load-bearing per \[D008.7\]" RULES.md = 32 (Phase 3's annotations preserved). - No production code modified. State updates: - state/beliefs.md: c025 (Phase 4 sweep ratified); c026 (model-id rule scope answered). - state/open_questions.md: q003, q009, q010 closed by c025/c026. Budget posture: Phase 4 spent 0 agents (main-thread). 4 remain for Phases 5 + 6. Next: Phase 5 (operational substrate — logging, scheduler, SQLite PRAGMA, last_stderr surfacing, bin/hanna-brief Python invocation; 3 agents). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
…SQLite PRAGMA (closes c011)
PRD Phase 5 of 6. Backs the README "always-on" claim operationally.
Phase 5 critic[verify] PASS on 16/16 acceptance criteria; 112/112
tests pass (was 107; +5 across TestApplyPragmas + tests/test_log.py).
Worker A — observability bundle:
- NEW src/_log.py — stdlib `get_logger(name)` with UTC ISO timestamps
(%(asctime)s.%(msecs)03dZ %(levelname)s %(name)s: %(message)s),
HANNA_LOG_LEVEL env support, idempotent root configuration. No deps.
- NEW src/__main__.py — `python3 -m src status` subcommand emits JSON
to stdout (hanna/harlo_reachable/harlo_burnout/ts/phase/
next_phase_boundary_eta_minutes) with logs to stderr; exit 0
regardless of harlo_reachable (state-blind is operational, not
failure).
- src/harlo_bridge.py — `get_logger("hanna.bridge")` at module top;
WARNING on every HarloUnreachable/HarloTimeout raise site (4
timeout + 2 EOF + others; 6 total in the new path);
`_log_stderr_tail(where)` helper surfaces the last 10 lines of
the previously-unread `_stderr_ring` at 12 lane-boundary failure
sites (the c011-named "diagnostic data no production caller
reads" — now read AND logged).
- scripts/first_hanna_brief.py — `get_logger("hanna.brief")` + INFO
at _phase_now/_read_harlo/_compose_brief/_persist/main exit;
WARNING + bridge.last_stderr() tail loop in both HarloUnreachable
and HarloTimeout branches of _read_harlo. NO logging in
src/computations/* (preserves pure-function discipline per Rule 36).
- NEW tests/test_log.py — 3 tests (get_logger format; status
subprocess JSON contract; _read_harlo logs WARNING with stderr
tail on Unreachable via caplog).
Worker B — durability + scheduling bundle:
- scripts/first_hanna_brief.py — NEW `_apply_pragmas(conn)` helper
(lines 59-69) executing journal_mode=WAL + synchronous=NORMAL +
busy_timeout=5000 + foreign_keys=ON; called at top of _persist()
immediately after sqlite3.connect. SCHEMA constant untouched;
idempotent re-application; second-writer race no longer immediate
SQLITE_BUSY (closes scout-security-rules M-3 + c028).
- bin/hanna-brief.command — Phase-2 swap: body now invokes
`python3 scripts/first_hanna_brief.py` from $REPO_ROOT with
PYTHONPATH set, tees output to ~/Library/Logs/hanna-brief.log
(macOS) or data/hanna-brief.log (Linux), propagates Python exit
code via PIPESTATUS. Comment header updated to mark Phase-2 as
current (Phase-1 static-HTML mockup preserved as historical).
- NEW bin/com.hanna.brief.morning.plist — launchd agent demonstrating
the MORNING schedule (Mon-Fri 09:00, RunAtLoad=false, Label
com.hanna.brief.morning, StandardOut/ErrorPath to
~/Library/Logs/hanna-brief.{stdout,stderr}.log). Valid XML;
parses cleanly via plistlib. XML-comment header documents
install/load/verify/unload commands. MIDDAY/EVENING/WEEKLY_MONDAY/
WEEKLY_FRIDAY/MONTHLY siblings are catalogued follow-ons.
- NEW bin/README.md — 31 lines; install/load/verify flow for the
launchd agent + manual launcher test + D011 macOS-only note;
Rule-36 voice (descriptive, not directive).
- TestApplyPragmas × 2 — disk DB round-trips journal_mode='wal'
after _persist; monkeypatch counts _apply_pragmas call.
MoE dispatch per D002:
- 2 workers in parallel (phase5a-observability + phase5b-durability-
scheduling) with strict file-ownership demarcation (Worker A:
src/_log.py + src/__main__.py + src/harlo_bridge.py logging +
scripts/first_hanna_brief.py logging hooks; Worker B: scripts/
first_hanna_brief.py _apply_pragmas + bin/). Zero scope collision
verified by critic[verify] criterion 16 (`grep -c "import logging
|get_logger|logger\." src/harlo_bridge.py` = 13, all Worker A;
bin/ + _apply_pragmas have no logging additions, all Worker B).
- 1 critic[verify] PASS 16/16.
Verified:
- python3 -m pytest tests/ -q → 112 passed in 0.63s.
- PYTHONPATH=. python3 scripts/first_hanna_brief.py → exit 0;
FAMILY_LOCKOUT path (Saturday 2026-05-30, expected).
- PYTHONPATH=. python3 -m src status → valid JSON, exit 0.
- xmllint --noout bin/com.hanna.brief.morning.plist → exit 0.
- Rule 35 enumerated bridge-surface allowlist still passes
(Phase-3 CI check); the D001 5-tool set preserved.
- Trailer hygiene clean; no model-id; stdlib only (no new deps).
- Rule 36 voice in log messages: surface, don't decide.
State updates:
- state/beliefs.md: c011 superseded by c027 (always-on now
operationally backed); c028 added (SQLite PRAGMA hardening
closes scout-security-rules M-3).
Budget posture: Phase 5 spent 3 agents (2 workers + 1 critic) of
the 4 remaining post-Phase-4. 1 agent remains for Phase 6.
Next: Phase 6 (data hygiene — ProductFile.parse validation
tightening per scout-code-quality M1; sub-render contract docs;
SQLite retention/cleanup policy outline). Single worker; main-thread
verify.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <[email protected]>
…e + sub-render contract + retention outline PRD Phase 6 of 6 — the cycle's final implementation phase + EXIT gate. Closes scout-code-quality M1 (ProductFile.parse silent- coercion gaps); strengthens c022 from empirical-only to documented; 117/117 tests pass. ProductFile.parse() — Rule-36 faithful-surface per c029: - Unknown `## X` section headers now emit a `hanna.schemas` WARNING via src/_log.get_logger (skip behavior preserved; drop made visible). - Duplicate frontmatter keys raise ValueError (was: second silently overwrote first). - Empty-content bullets (`-` or `- `) are explicitly dropped (was: included as empty strings). - 64KB input cap raises ValueError on oversized input (matches the stderr-ring 64-line discipline; product files aren't expected large). Sub-render contract — c030 documents what c022 named: - scripts/first_hanna_brief.py module docstring §"Sub-render contract" states: each helper carries its own trailing punctuation; composer joins with single space; empty-signal returns "" so the space-join collapses cleanly. Future composer extensions can cite the contract rather than reverse-engineer it. Data retention outline — bin/README.md gains §"Data retention": - briefs table is append-only by design; D012 idempotency prevents duplicate rows. - Joe's expected volume: ~10 briefs/week × ~2KB body = ~1MB/year; no active retention for first 5 years. - Future retention hook: `python3 -m src vacuum --keep-days N` catalogued for L6+ (not implemented). - Backup posture: data/hanna.sqlite is gitignored; iCloud Drive / Time Machine if Joe wants backups (Hanna doesn't manage backup). - No SQLite encryption / TDE; corpus is non-sensitive. Tests added — 5 new in tests/test_schemas.py TestProductFile: - test_parse_unknown_section_logs_warning - test_parse_duplicate_frontmatter_key_raises - test_parse_skips_empty_bullets - test_parse_input_size_cap_raises - test_parse_input_just_under_cap_succeeds MoE dispatch per D002: - 1 worker (phase6-data-hygiene); orchestrator verifies main-thread (budget-exhausted; per-phase critic verify pattern preserved as workflow; pytest 117/117 + grep audits substitute for the spawn). Verified: - python3 -m pytest tests/ -q → 117 passed in 0.64s (was 112; +5). - grep verifies: 4 new validation primitives in src/schemas.py; "Sub-render contract" present in scripts/first_hanna_brief.py:8; "Data retention" present in bin/README.md. - Trailer hygiene clean; no model-id; stdlib only. State updates: - state/beliefs.md: c029 added (ProductFile.parse faithful-surface); c030 added (sub-render contract documented). EXIT GATE WRITTEN — state/gate.md: - TERMINATION: exit (positive — GOAL satisfied) - All EXIT_CRITERIA met: 7 scout findings, PRD authored, beliefs + open_questions populated, Joe presented + "go" confirmed, Phase 0–6 implementation complete, budget 13/13 spent of 20-cap, 117/117 tests green, all critic[verify] PASS where dispatched. - FINAL_BELIEFS: c015–c030 list (15 active claims from this cycle). - OPEN_RESIDUAL: q001 (Octavius IPC; L7), q002 (LockoutResponse; high, gates L6), q007 (composition boundary; high, gates L6), q008 (scheduler beyond MORNING; medium), q011 (DECISIONS template; low), q012 (L5 schema split; medium). This commit closes the orchestrator's first live GOAL: scout (7 agents) → PRD → "go" → 6-phase implementation (13 agents total) → critic[verify] PASS at every phase → final EXIT gate. Next session entry point: L4b dispatch — now unblocked by Phase 1's D010/D011/D012 ratifications + Phase 2's frame-coalescing regression test + Phase 5's operational substrate. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
📝 WalkthroughWalkthroughThis PR adds a multi-agent orchestrator manual and agent specs, extends BriefPayload with ET-anchored phase_anchor_iso and brief_id, hardens SQLite persistence (PRAGMAs, INSERT OR IGNORE), instruments logging/Harlo probes, implements a macOS Calendar channel with idempotent publish/archive, tightens CI Rule 35/37 checks, expands tests (frame coalescing, phase precedence, reconciliation), and adds extensive state/planning/audit documents and launch artifacts. ChangesOrchestrator Framework & Observability Hardening
Estimated code review effort 🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Poem
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
|
… "Hanna is real" arc
Joe ratified SPEC.md inline ("ratified") — FRAME gate cleared. This commit
lands the SKETCH gate deliverables: the per-GOAL K+S harness workspace
under state/tasks/hanna-real-arc/, plus the cross-GOAL LEDGER.
Per the harness's own discipline:
- The K+S harness is ON PROBATION for this arc only. D014 ratification
(permanent adoption) waits until this arc verifies its keep.
- ORCHESTRATOR.md §7 already sanctions per-GOAL runtime state files
(the Phase 0 amendment); state/tasks/hanna-real-arc/ fits cleanly.
- state/beliefs.md + state/open_questions.md + state/LEDGER.md are the
durable cross-GOAL layer; the K+S workspace is per-GOAL.
Files created (9):
- state/tasks/hanna-real-arc/SPEC.md — ratified contract; P1–P10
acceptance predicates; F1–F6 falsification conditions; per-predicate
verification matrix (L0–L4 + stochastic?); effort budget = 50 agents
for the whole arc.
- state/tasks/hanna-real-arc/CHAMPION.md — seed champion = seed-2118024
(current HEAD); predicate-score 0.27/1.00; reproduction recipe;
promotion rule (strict increase ≥1 predicate, no regression).
- state/tasks/hanna-real-arc/PLAN.md — 3 lines open (A=L4b code,
B=L5 schemas + D014/D015 substrate decisions, C=Octavius reachability
spike); D + E held by dependency. Per-line: GOAL · CONTRACT · VERIFIER
· ranked proposal queue. Mode statement (ORCHESTRATED TEAM). REORGANIZE
triggers documented.
- state/tasks/hanna-real-arc/DIGEST.md — cycle-0 SKETCH→DELIBERATE
handoff. The sketch ASCII diagram. Load-bearing components × riskiest
unknowns. Dependency graph. Confidence per SPEC predicate (mean 0.74;
riskiest P4 Octavius 0.40; most-stochastic P10 trial 0.55). Open
questions including 2 new (q014 secret-storage, q015 HMAC rotation).
- state/tasks/hanna-real-arc/LOG.md — seeded with cycle-0 baseline
row (seed champion declared).
- state/tasks/hanna-real-arc/FORUM.md — SKETCH-time standing
critiques (Line A/B/C top-proposals each survived initial critique).
- state/tasks/hanna-real-arc/DEADENDS.md — empty (arc just opened).
- state/tasks/hanna-real-arc/TRACE.md — gate-cross + mode-change +
branch-decisions recorded.
- state/LEDGER.md (NEW; cross-GOAL durable) — seeded with 5 recipes that
survived the prior cycle's verification: canonical-commit-trailer,
moe-dispatch-d002, belief-supersession, pytest-as-l1,
ci-rule-allowlist-pattern.
Complexity Gate decision (per harness):
- BREADTH=high (3 effective independent lines initially, opens to 4)
- INDEPENDENCE=medium (graph-predicted; shared-write contention on
docs/DECISIONS.md, src/schemas.py, LOG.md — claim/lock tractable)
- HORIZON=long (multi-cycle arc, ~50-agent budget)
- REWORK COST=high (D-entries append-only-with-supersede; schema
changes cascade into tests)
- VERIFIER COST=medium (pytest fast; CI ~13s; 7-day Mac trial expensive
but terminal-only)
- External launcher available (Claude Code's Agent tool, proven by the
prior cycle's 7-parallel-scout dispatch)
→ ORCHESTRATED TEAM mode declared. Honesty constraint honored:
single-context-narrating-parallel is honest because real parallel
dispatch happens through the tool.
CONTENTION PROBE skipped (mode plausibly obvious; flagged per the
HONESTY CONSTRAINT — trading observed contention for graph prediction).
Next gate: DELIBERATE (cycle 1) — Analyst + Critic open Lines A/B/C
top-proposals on the FORUM for critique-before-build; weak proposals
die there; the surviving queue feeds EXECUTE.
No code changed; no tests added or removed (117/117 still green); no
production behavior delta. This commit is the harness-workspace
scaffold + ratified contract on disk.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <[email protected]>
There was a problem hiding this comment.
Actionable comments posted: 17
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
scripts/first_hanna_brief.py (1)
199-216:⚠️ Potential issue | 🟠 Major | ⚡ Quick winCapture the compose instant once before deriving the anchor and idempotency key.
composed_at_isoandcompose_datecome from separatenow()calls. If execution crosses an ET date/phase boundary here, the persisted timestamp can describe one brief whilephase_anchor_iso/brief_iddescribe another, which breaks the dedupe contract at the exact boundary you're trying to stabilize.Suggested fix
- composed_at = datetime.now(timezone.utc).isoformat() + composed_at_dt = datetime.now(timezone.utc) + composed_at = composed_at_dt.isoformat() @@ - compose_date = datetime.now(ZoneInfo("America/New_York")).date() + compose_date = composed_at_dt.astimezone(ZoneInfo("America/New_York")).date()🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/first_hanna_brief.py` around lines 199 - 216, Capture the current instant once and reuse it for the composed timestamp and for deriving the Eastern Time compose_date and phase anchor so all ids/timestamps come from the same moment: replace the separate now() calls by a single timestamp (e.g., composed_at or composed_at_iso) and compute compose_date = composed_instant.astimezone(ZoneInfo("America/New_York")).date(), then call BriefPayload.compute_phase_anchor_iso(phase, compose_date) and BriefPayload.compute_brief_id(phase, phase_anchor_iso, list(ranked)) using that same composed instant; update any uses of composed_at/composed_at_iso, phase_anchor_iso, and brief_id to rely on the single captured instant.
🧹 Nitpick comments (4)
tests/test_first_hanna_brief.py (1)
452-482: ⚡ Quick winAvoid pinning this unit test to the current
data/products/contents.Asserting the exact four on-disk product names makes this test fail whenever someone adds or renames an unrelated product file, even if
_compose_brief()still behaves correctly. Stub_read_product_files()/PRODUCTS_DIR, or assert only the stable invariants this function is meant to guarantee.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/test_first_hanna_brief.py` around lines 452 - 482, The test test_state_blind_morning_returns_populated_brief is brittle because it asserts an exact list of on-disk product names; either stub _read_product_files (or set PRODUCTS_DIR) to return a deterministic list before calling _compose_brief so referenced_products is stable, or relax the assertion to check stable invariants (e.g. assert brief.referenced_products is non-empty and contains expected required products like "harlo" rather than exact equality). Update the test to use monkeypatch to replace _read_product_files() with a fixed list or replace the sorted equality assertion with checks such as len(brief.referenced_products) > 0 and "harlo" in brief.referenced_products.state/tasks/hanna-real-arc/PLAN.md (1)
16-16: 💤 Low valueInconsistent macOS terminology.
Line 16 uses
non-macin the quoteHannaCalendarNotAvailableexception name, but the standard term from Apple and used elsewhere in the codebase ismacOS. While this is in a proposed exception class name (not yet implemented), consider usingnon_macosto match the convention established in SPEC.md line 27 (unpublished_reasonvalues likenon_macos).📝 Suggested naming consistency
-1. Author `src/channels/__init__.py` + `src/channels/calendar.py` with `publish(brief)` + `archive(event_id)` per ROADMAP §4 L4b spec, honoring D010 (`brief.phase_anchor_iso` as event-start) + D011 (`HannaCalendarNotAvailable` on non-mac) + D012 (lookup by `brief_id` before insert) + 0-min anchor events. +1. Author `src/channels/__init__.py` + `src/channels/calendar.py` with `publish(brief)` + `archive(event_id)` per ROADMAP §4 L4b spec, honoring D010 (`brief.phase_anchor_iso` as event-start) + D011 (`HannaCalendarNotAvailable` on non-macOS) + D012 (lookup by `brief_id` before insert) + 0-min anchor events.Or use
non_macosif the exception class will be namedHannaCalendarNotAvailableNonMacOSor similar.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@state/tasks/hanna-real-arc/PLAN.md` at line 16, The comment flags inconsistent macOS terminology: replace the phrase "non-mac" with the established "non_macos" form wherever it refers to the HannaCalendarNotAvailable condition in your plan and future code; update the proposed exception/constant naming (e.g., HannaCalendarNotAvailable, unpublished_reason values) to use non_macos to match SPEC.md conventions and existing codebase usage so lookup/serialization and any checks against `brief.unpublished_reason` remain consistent.state/tasks/hanna-real-arc/CHAMPION.md (1)
31-31: 💤 Low valueVerify P7 score precision.
The predicate table shows P7 with a score of 0.59, but the calculation is 117 / 200 = 0.585. While the rounding difference is minor (0.005), the champion score calculation depends on these values. Consider using 0.585 or 0.59 consistently with a stated rounding policy.
📊 Suggested precision fix
-| P7 (≥200 tests) | **117 / 200** | 0.59 | +| P7 (≥200 tests) | **117 / 200** | 0.585 |Or document the rounding policy if 0.59 is intentional (e.g., "scores rounded to nearest 0.01").
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@state/tasks/hanna-real-arc/CHAMPION.md` at line 31, The P7 predicate row shows a displayed score of 0.59 but mathematically 117/200 = 0.585; update the P7 table entry (the row containing "P7 (≥200 tests) | **117 / 200** | 0.59") to use a consistent value (either change the displayed score to 0.585 or change calculations that consume this value to use 0.59) and add a brief rounding policy note (e.g., "scores rounded to nearest 0.01") so the champion score computation and the table use the same precision.ORCHESTRATOR.md (1)
20-20: 💤 Low valueOptional: Use Hanna-specific path for consistency.
Line 20 references
decisions.md, but §7 (line 299) maps this todocs/DECISIONS.mdin Hanna. Consider using the concrete path here for clarity, or add a forward reference to §7.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@ORCHESTRATOR.md` at line 20, Update the reference on line 20 that currently says "decisions.md" to use the concrete Hanna path "docs/DECISIONS.md" (or append a forward reference to §7) so the doc is consistent with the mapping defined in §7; locate the string "decisions.md" in ORCHESTRATOR.md and replace it with "docs/DECISIONS.md" or add "(see §7 `docs/DECISIONS.md`)" to clarify the location.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/ci.yml:
- Around line 67-69: The exclusion using grep -vEi that currently filters out
any line containing the substring "Rule 37" is too broad; update the negative
filter(s) used when populating the hits variable (and the similar filters at the
later occurrences) to only exclude exact self-referential phrasings such as the
canonical rule text (e.g., "Rule 37: Never raise patent topics", "Rule 37: Never
raise patent topics. No exceptions") or tightly scoped variants like lines that
start with "Rule 37:" rather than any line containing "Rule 37"; locate the grep
-vEi expressions in the hits assignment and the related blocks (the patterns
that include "Rule 37|...") and replace the broad "Rule 37" alternation with
precise, anchor- or full-phrase patterns so legitimate matches mentioning Rule
37 in context are not suppressed.
- Around line 30-33: CI currently only blocks a few octavius.* write verbs;
update .github/workflows/ci.yml to mirror the harlo_bridge allowlist logic for
src/octavius_bridge.py so that any octavius.* usage outside that bridge is
rejected unless it is one of the allowed verbs (spawn|poll|harvest).
Specifically, add the same file-scope allowlist check that references
src/octavius_bridge.py and enforces only spawn/poll/harvest calls (Rule 35) by
failing the job when other octavius.* operations are found in source files.
In `@bin/com.hanna.brief.morning.plist`:
- Around line 40-82: The StartCalendarInterval entries (Weekday/Hour/Minute)
schedule the MORNING job in the host's local wall-clock TZ so 09:00 will drift
if the machine isn't in America/New_York; to fix, add a runtime timezone gate:
at process startup check the system timezone (e.g., via tzlocal or
/etc/localtime) and if it's not "America/New_York" either abort with a clear
error or reschedule (fail-fast/gate), or alternatively change the plist to a
broader local trigger and update the Python compose/persist logic to compute the
next run time in ET before composing/persisting (i.e., detect
StartCalendarInterval firing, then in your MORNING job code convert now to
America/New_York and only proceed if the ET-aligned hour/minute matches);
reference StartCalendarInterval, Weekday/Hour/Minute and the MORNING job
compose/persist logic when implementing the check/gating.
In `@docs/DECISIONS.md`:
- Line 617: The D010 cross-reference fragment is invalid and breaking markdown
navigation; update the link target for D010 in the Related list so it matches
the actual generated anchor or replace it with a plain section reference: either
correct the fragment to the exact slugified header for "D010 -- l4b rhythm
anchor events land at 0900 et phase anchor not compose moment" (matching how
your markdown processor lowercases, removes punctuation and spaces) or change
the link text to "D010" followed by the full section title as plain text so the
reference works without a fragment.
- Around line 483-490: The D009 entry currently claims only state/beliefs.md and
state/open_questions.md are new but the PR also adds state/plan.md,
state/checkpoint.md, state/gate.md and state/LEDGER.md, so update D009
(D009.1/D009.2) to explicitly classify each file as either a standing alias
(e.g., DECISIONS.md, ROADMAP.md, NEXT.md mappings) or a per-GOAL/runtime state
file (list the added files state/plan.md, state/checkpoint.md, state/gate.md,
state/LEDGER.md) and ensure ORCHESTRATOR.md and CLAUDE.md pointer text reflects
that separation; mention the belief layer (state/beliefs.md) remains
single-writer and subagent deltas are proposals so the ratified contract matches
the actual files landed.
In `@NEXT.md`:
- Around line 3-7: The "2026-05-22 (post-merge) — buildout chapter closed"
section currently mixes a preserved historical snapshot with the live "Where you
are" handoff; separate them by moving the PR `#1` / branch consumed / main HEAD =
e08aebb619... bullets into a clearly labeled historical section (under the
existing "2026-05-22" heading) and ensure the present-tense "Where you are"
block contains the actual current PR/branch/HEAD info for PR `#2`
(claude/hanna-mcp-review-ZsorY), updating those bullets accordingly so the live
handoff reflects the current session state rather than the archived snapshot.
In `@RULES.md`:
- Around line 213-219: The doc currently misstates the lockout layers as “two
active layers” while also saying Layer 3 is deferred and tests reference three
layers; update the Family hours paragraph to consistently describe which layers
are active vs deferred: state the state machine compute_producer_phase returns
FAMILY_LOCKOUT (active), note HdProducer delegate routing was cut per D008.1 and
collapsed into per-tool lockout (mark as removed/cut), and clearly mark MCP tool
gating (mcp_tools lane / LockoutResponse) as the second active layer with
Layer-3 behavior deferred to L6 per ROADMAP.md; also update the test statement
and any reference to a “three-layer” lockout test (line ~281) to say tests
verify the two active layers and that the Layer-3 test scaffold is deferred to
L6, and ensure terminology (compute_producer_phase, HdProducer, LockoutResponse,
override_token) is used consistently.
In `@src/__main__.py`:
- Around line 57-68: The Harlo read_state call can block indefinitely because
startup_timeout_seconds only covers HarloBridge initialization; run
bridge.read_state inside a worker and enforce a timeout (e.g., use
concurrent.futures.ThreadPoolExecutor to submit bridge.read_state and call
future.result(timeout=2.0)), catch concurrent.futures.TimeoutError and map/log
it as a HarloTimeout (or raise HarloTimeout) so the existing except block
(HarloUnreachable, HarloTimeout, HarloProtocolError) will handle it, and add the
required import for concurrent.futures; keep HarloBridge, read_state,
HarloTimeout, HarloUnreachable, HarloProtocolError, and logger references when
making the change.
In `@src/_log.py`:
- Around line 30-47: Remove the global class-level mutation
logging.Formatter.converter = time.gmtime in _configure_once(); instead, create
Formatter instances and set their converter attribute per-instance (e.g., after
creating formatter = logging.Formatter(fmt=_FORMAT, datefmt=_DATEFMT) do
formatter.converter = time.gmtime) before passing them to handlers and before
using them with logging.basicConfig’s handlers if you replace or set handlers;
ensure every formatter you construct (the one applied to each handler in the
root.handlers loop and any formatter used for initial configuration) has
formatter.converter set to time.gmtime so the UTC timestamp behavior is local to
our formatters only.
In `@src/harlo_bridge.py`:
- Around line 181-192: The stderr ring buffer (_stderr_ring) is never cleared so
_log_stderr_tail() can surface stale stderr from previous Harlo processes; to
fix, clear or reinitialize self._stderr_ring when a Harlo process lifecycle ends
(call clear() or recreate the deque) inside the close() method and also
immediately before spawning a replacement process (e.g., in whatever method
starts/respawns Harlo, such as the spawn/start/_ensure_running code path) so
each new subprocess begins with an empty buffer and diagnostic output only
reflects the current process.
In `@src/schemas.py`:
- Around line 231-234: The code currently builds payload from
sorted(referenced_products) which preserves duplicates; change it to hash the
product-name set instead by deduplicating referenced_products first (e.g., use
set(referenced_products) or an equivalent) then sort that set and join into
sorted_products so the payload built from phase.name, anchor_date_iso and
sorted_products is stable and duplicate-insensitive (refer to variables
phase_anchor_iso, referenced_products, sorted_products, payload).
- Around line 80-81: The guard in ProductFile.parse currently uses len(text)
which counts characters, not UTF-8 bytes, so replace that check to measure the
encoded byte length (e.g., len(text.encode("utf-8"))) and compare against
_PARSE_INPUT_CAP_BYTES; update the conditional that raises
ValueError("ProductFile.parse: input exceeds 64KB cap") to use the UTF-8 byte
length of text to correctly enforce the byte cap.
In `@state/beliefs.md`:
- Line 22: The belief entry c017 declares SUPERSEDES: c012 but c012 still shows
SUPERSEDED_BY: none and STATUS: active, and c017's EVIDENCE incorrectly says
"Strengthens c012"; update c012 to set SUPERSEDED_BY: c017 and change STATUS to
superseded (or equivalent per ORCHESTRATOR.md), and edit c017's EVIDENCE to
indicate it supersedes/replaces c012 (e.g., "Supersedes c012: confirms
idempotency") so the supersession chain and wording are consistent with the
single-writer integrity contract; ensure you modify the fields named
SUPERSEDED_BY, STATUS in the c012 entry and the EVIDENCE field in the c017
entry.
In `@state/gate.md`:
- Line 10: Update the commit count statement in the durable EXIT receipt: change
the text "Implementation cycle Phase 0–6 complete: met — 7 commits (one per
phase + the synthesis checkpoint that preceded)" to reflect eight commits (e.g.
"… — 8 commits") so the numeric total matches the receipt list; ensure the
parenthetical clarification correctly indicates the checkpoint plus the phases
and the final EXIT commit.
In `@state/LEDGER.md`:
- Line 9: Remove the exception that allows omitting the canonical commit trailer
in the ledger row identified by canonical-commit-trailer: delete the "when the
user explicitly says 'drop the trailer'" clause so the rule always requires
appending the canonical Claude Code trailer (`🤖 Generated with [Claude
Code](https://claude.com/claude-code)` followed by `Co-Authored-By: Claude
<noreplyanthropic.com>`) to every commit; ensure the row's condition column only
reflects that the trailer is mandatory and update any related explanatory text
referencing canonical-commit-trailer to match the mandatory policy.
In `@tests/test_log.py`:
- Around line 65-94: The test assumes harlo is absent from PATH; make the
subprocess deterministic by passing an explicit env to subprocess.run in
test_status_subprocess_returns_valid_json_with_expected_keys: copy os.environ,
set env["PATH"] to a known-empty or bogus path (e.g., "" or a tmp dir) so the
harlo lookup always fails, then pass that env to subprocess.run; update the call
in the test function (the subprocess.run invocation) to include env=env (and
preserve other necessary env vars from os.environ.copy()) so the unreachable
branch is deterministic.
---
Outside diff comments:
In `@scripts/first_hanna_brief.py`:
- Around line 199-216: Capture the current instant once and reuse it for the
composed timestamp and for deriving the Eastern Time compose_date and phase
anchor so all ids/timestamps come from the same moment: replace the separate
now() calls by a single timestamp (e.g., composed_at or composed_at_iso) and
compute compose_date =
composed_instant.astimezone(ZoneInfo("America/New_York")).date(), then call
BriefPayload.compute_phase_anchor_iso(phase, compose_date) and
BriefPayload.compute_brief_id(phase, phase_anchor_iso, list(ranked)) using that
same composed instant; update any uses of composed_at/composed_at_iso,
phase_anchor_iso, and brief_id to rely on the single captured instant.
---
Nitpick comments:
In `@ORCHESTRATOR.md`:
- Line 20: Update the reference on line 20 that currently says "decisions.md" to
use the concrete Hanna path "docs/DECISIONS.md" (or append a forward reference
to §7) so the doc is consistent with the mapping defined in §7; locate the
string "decisions.md" in ORCHESTRATOR.md and replace it with "docs/DECISIONS.md"
or add "(see §7 `docs/DECISIONS.md`)" to clarify the location.
In `@state/tasks/hanna-real-arc/CHAMPION.md`:
- Line 31: The P7 predicate row shows a displayed score of 0.59 but
mathematically 117/200 = 0.585; update the P7 table entry (the row containing
"P7 (≥200 tests) | **117 / 200** | 0.59") to use a consistent value (either
change the displayed score to 0.585 or change calculations that consume this
value to use 0.59) and add a brief rounding policy note (e.g., "scores rounded
to nearest 0.01") so the champion score computation and the table use the same
precision.
In `@state/tasks/hanna-real-arc/PLAN.md`:
- Line 16: The comment flags inconsistent macOS terminology: replace the phrase
"non-mac" with the established "non_macos" form wherever it refers to the
HannaCalendarNotAvailable condition in your plan and future code; update the
proposed exception/constant naming (e.g., HannaCalendarNotAvailable,
unpublished_reason values) to use non_macos to match SPEC.md conventions and
existing codebase usage so lookup/serialization and any checks against
`brief.unpublished_reason` remain consistent.
In `@tests/test_first_hanna_brief.py`:
- Around line 452-482: The test test_state_blind_morning_returns_populated_brief
is brittle because it asserts an exact list of on-disk product names; either
stub _read_product_files (or set PRODUCTS_DIR) to return a deterministic list
before calling _compose_brief so referenced_products is stable, or relax the
assertion to check stable invariants (e.g. assert brief.referenced_products is
non-empty and contains expected required products like "harlo" rather than exact
equality). Update the test to use monkeypatch to replace _read_product_files()
with a fixed list or replace the sorted equality assertion with checks such as
len(brief.referenced_products) > 0 and "harlo" in brief.referenced_products.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 8ef1ecee-c78d-4fa9-9a01-499e94c13c04
📒 Files selected for processing (48)
.claude/agents/critic.md.claude/agents/integrator.md.claude/agents/planner.md.claude/agents/worker.md.github/workflows/ci.ymlCLAUDE.mdNEXT.mdORCHESTRATOR.mdREADME.mdRULES.mdbin/README.mdbin/com.hanna.brief.morning.plistbin/hanna-brief.commanddocs/DECISIONS.mddocs/REVIEW_2026-05-22.mddocs/SESSION_01_RECON.mdscripts/first_hanna_brief.pysrc/__main__.pysrc/_log.pysrc/harlo_bridge.pysrc/schemas.pystate/LEDGER.mdstate/beliefs.mdstate/checkpoint.mdstate/gate.mdstate/open_questions.mdstate/plan.mdstate/tasks/hanna-real-arc/CHAMPION.mdstate/tasks/hanna-real-arc/DEADENDS.mdstate/tasks/hanna-real-arc/DIGEST.mdstate/tasks/hanna-real-arc/FORUM.mdstate/tasks/hanna-real-arc/LOG.mdstate/tasks/hanna-real-arc/PLAN.mdstate/tasks/hanna-real-arc/SPEC.mdstate/tasks/hanna-real-arc/TRACE.mdstate/tasks/prd/PRD.mdstate/tasks/scout-architecture/findings.mdstate/tasks/scout-code-quality/findings.mdstate/tasks/scout-docs/findings.mdstate/tasks/scout-lanes-schemas/findings.mdstate/tasks/scout-ops/findings.mdstate/tasks/scout-security-rules/findings.mdstate/tasks/scout-tests/findings.mdtests/computations/test_compute_producer_phase.pytests/test_first_hanna_brief.pytests/test_harlo_bridge.pytests/test_log.pytests/test_schemas.py
| if grep -rE "octavius\.(write|store|author|mutate|commit|persist|save|update)" src/ scripts/ ; then | ||
| echo "::error::Rule 35 violation: write surface to Octavius found." | ||
| exit 1 | ||
| fi |
There was a problem hiding this comment.
Rule 35 is still unenforced for the Octavius surface.
This only blocks a few write-like octavius.* verbs, while the authoritative allowlist exists only for src/harlo_bridge.py. A disallowed Octavius operation like cancel, list, or any future non-spawn/poll/harvest call would still pass CI. Mirror the bridge-surface allowlist check for src/octavius_bridge.py so the workflow enforces the same contract it documents.
As per coding guidelines, **/*.py: Request Octavius via src/octavius_bridge.py only; only spawn/poll/harvest operations allowed (Rule 35).
Also applies to: 40-59
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/ci.yml around lines 30 - 33, CI currently only blocks a
few octavius.* write verbs; update .github/workflows/ci.yml to mirror the
harlo_bridge allowlist logic for src/octavius_bridge.py so that any octavius.*
usage outside that bridge is rejected unless it is one of the allowed verbs
(spawn|poll|harvest). Specifically, add the same file-scope allowlist check that
references src/octavius_bridge.py and enforces only spawn/poll/harvest calls
(Rule 35) by failing the job when other octavius.* operations are found in
source files.
| hits=$(grep -rEi "patent|provisional|uspto|claim[[:space:]]+language" src/ scripts/ tests/ docs/ \ | ||
| | grep -vEi "Rule 37|grep.*patent|patent[- ]topic|patent[- ]adjacent|PATENTS\.md|no patent" \ | ||
| || true) |
There was a problem hiding this comment.
The Rule 37 exemption is broad enough to hide real violations.
Both filters drop any matching line that also contains Rule 37, so content like Rule 37: add patent triage would be exempted instead of failing CI. Narrow these exclusions to exact self-referential phrasings rather than a blanket Rule 37 substring.
Based on learnings, Rule 37: Never raise patent topics. No exceptions in any session, commit message, or doc generated under this repo.
Also applies to: 84-87
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/ci.yml around lines 67 - 69, The exclusion using grep -vEi
that currently filters out any line containing the substring "Rule 37" is too
broad; update the negative filter(s) used when populating the hits variable (and
the similar filters at the later occurrences) to only exclude exact
self-referential phrasings such as the canonical rule text (e.g., "Rule 37:
Never raise patent topics", "Rule 37: Never raise patent topics. No exceptions")
or tightly scoped variants like lines that start with "Rule 37:" rather than any
line containing "Rule 37"; locate the grep -vEi expressions in the hits
assignment and the related blocks (the patterns that include "Rule 37|...") and
replace the broad "Rule 37" alternation with precise, anchor- or full-phrase
patterns so legitimate matches mentioning Rule 37 in context are not suppressed.
| <key>StartCalendarInterval</key> | ||
| <array> | ||
| <dict> | ||
| <key>Weekday</key> | ||
| <integer>1</integer> | ||
| <key>Hour</key> | ||
| <integer>9</integer> | ||
| <key>Minute</key> | ||
| <integer>0</integer> | ||
| </dict> | ||
| <dict> | ||
| <key>Weekday</key> | ||
| <integer>2</integer> | ||
| <key>Hour</key> | ||
| <integer>9</integer> | ||
| <key>Minute</key> | ||
| <integer>0</integer> | ||
| </dict> | ||
| <dict> | ||
| <key>Weekday</key> | ||
| <integer>3</integer> | ||
| <key>Hour</key> | ||
| <integer>9</integer> | ||
| <key>Minute</key> | ||
| <integer>0</integer> | ||
| </dict> | ||
| <dict> | ||
| <key>Weekday</key> | ||
| <integer>4</integer> | ||
| <key>Hour</key> | ||
| <integer>9</integer> | ||
| <key>Minute</key> | ||
| <integer>0</integer> | ||
| </dict> | ||
| <dict> | ||
| <key>Weekday</key> | ||
| <integer>5</integer> | ||
| <key>Hour</key> | ||
| <integer>9</integer> | ||
| <key>Minute</key> | ||
| <integer>0</integer> | ||
| </dict> | ||
| </array> |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Does macOS launchd StartCalendarIntervalrun in the user's local timezone, and can a LaunchAgent plist pin execution toAmerica/New_York?
💡 Result:
Yes—StartCalendarInterval is evaluated using the system’s local time (i.e., “wall clock” time as expressed by the machine’s current time zone). Apple’s launchd.plist semantics describe scheduling “calendar intervals” similarly to crontab, and the observed behavior is tied to local clock/timezone changes rather than allowing per-job timezone pinning.[1][2][3] No—there is no supported plist key to “pin” a single LaunchAgent’s StartCalendarInterval execution to an arbitrary IANA time zone like America/New_York. The launchd.plist(5) dictionary for StartCalendarInterval consists only of calendar fields such as Minute, Hour, Day, Weekday, and Month (wildcards for missing fields), with no timezone selector; therefore timezone control is not provided at that level.[1][2] Practical implication: if you want jobs to always run at “9:00 AM America/New_York” regardless of the machine’s current time zone, launchd alone (via StartCalendarInterval) can’t directly express that. You’d need to implement your own timezone logic (e.g., compute the next run time in your script) or run with a different scheduling mechanism that supports explicit time zones (but that is outside what launchd’s StartCalendarInterval schema provides).[1][2] Citations: - launchd.plist(5) describes StartCalendarInterval fields (Minute/Hour/Day/Weekday/Month) and notes its cron-like semantics, but does not include any timezone parameter.[1][2] - Apple’s system startup/scheduled jobs docs point to launchd.plist for the calendar values and describe the wake-up coalescing behavior, again without any timezone pinning feature.[4] - Community Q&A discusses DST/time handling for StartCalendarInterval in terms of local time behavior (no per-job TZ control), reinforcing that the schedule follows the system’s timezone behavior.[5]
Citations:
- 1: https://keith.github.io/xcode-man-pages/launchd.plist.5.html
- 2: https://www.manpagez.com/man/5/launchd.plist/osx-10.6.php
- 3: https://dev.to/lumm_7b4812001a80f4b531cf/7-cron-expression-gotchas-that-will-silently-break-your-scheduled-jobs-5f6a
- 4: https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/ScheduledJobs.html
- 5: https://apple.stackexchange.com/questions/121416/how-daylight-saving-changes-are-handled-in-launchd-to-make-sure-it-execute-the-t
Major: StartCalendarInterval schedules in host local time (no per-plist timezone pin), so the 09:00 “ET” anchor can drift
In bin/com.hanna.brief.morning.plist (lines 40-82), the weekday runs with <Hour>9</Hour> / <Minute>0</Minute> will fire at 09:00 in the Mac’s current timezone, because launchd StartCalendarInterval has no timezone selector (it follows wall-clock/local timezone). That means timezone changes (travel/DST/system TZ) can shift the MORNING job off the D010/ET phase anchor and lead to composing/persisting the wrong phase.
Fix by either:
- Fail fast / gate: detect the host timezone at runtime and abort or reschedule when it’s not
America/New_York(ET), or - Timezone-agnostic schedule + ET gating: schedule using a broader local trigger and let the Python logic compute “next run time in ET” before composing/persisting.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@bin/com.hanna.brief.morning.plist` around lines 40 - 82, The
StartCalendarInterval entries (Weekday/Hour/Minute) schedule the MORNING job in
the host's local wall-clock TZ so 09:00 will drift if the machine isn't in
America/New_York; to fix, add a runtime timezone gate: at process startup check
the system timezone (e.g., via tzlocal or /etc/localtime) and if it's not
"America/New_York" either abort with a clear error or reschedule
(fail-fast/gate), or alternatively change the plist to a broader local trigger
and update the Python compose/persist logic to compute the next run time in ET
before composing/persisting (i.e., detect StartCalendarInterval firing, then in
your MORNING job code convert now to America/New_York and only proceed if the
ET-aligned hour/minute matches); reference StartCalendarInterval,
Weekday/Hour/Minute and the MORNING job compose/persist logic when implementing
the check/gating.
| - **D009.1 — Generalize & reuse (not replace, not coexist).** The orchestrator is the umbrella; D002 is its role taxonomy. The generic state names are aliases for Hanna's existing files (`decisions.md`→`docs/DECISIONS.md`, `plan.md`→`docs/ROADMAP.md` §5 + the `NEXT.md` GOAL block, `checkpoint.md`→`NEXT.md`, `parked.md`→`NEXT.md` + `state/open_questions.md`) — no duplicate standing files. Only the two genuinely-new artifacts (`state/beliefs.md`, `state/open_questions.md`) are created. `/hanna-dispatch-next` becomes one workflow under the orchestrator (ORCHESTRATOR.md §8). No teardown of existing machinery. | ||
| - **D009.2 — Separate `ORCHESTRATOR.md`.** The operating manual lives in its own root file; `CLAUDE.md` gains a pointer. The manual (how an agent behaves) stays separate from the project instructions (what Hanna is). | ||
|
|
||
| **Reasoning.** An inventory found Hanna already implements ~70% of the framework in bespoke form — D002 roles map 1:1 to planner/worker/critic/integrator, and DECISIONS/ROADMAP/NEXT map to decisions/plan/checkpoint. The genuinely-missing piece is the belief layer, which would have made the 2026-05-22 session's repeated "done → superseded-by-deeper-review" pattern (CodeRabbit rounds 1→2→3; the frame-coalescing bug) visible instead of silent. Generalize & reuse captures that value with the least churn; the adapter mapping avoids the dual-system tax of "coexist" and the teardown risk of "replace." | ||
|
|
||
| **Implications.** | ||
| - 7 files created (ORCHESTRATOR.md, state/beliefs.md, state/open_questions.md, 4 `.claude/agents/*.md`); CLAUDE.md gains an Orchestration pointer block; this D009 entry ratifies it. | ||
| - The belief layer is single-writer (orchestrator only); subagents propose deltas in summaries. |
There was a problem hiding this comment.
Make D009 distinguish standing aliases from runtime state files.
This entry says only state/beliefs.md and state/open_questions.md are genuinely new and then counts 7 created files, but the same PR also lands state/plan.md, state/checkpoint.md, state/gate.md, and state/LEDGER.md. Please spell out which files are standing aliases versus per-GOAL/runtime state so the ratified contract matches the adopted surface.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/DECISIONS.md` around lines 483 - 490, The D009 entry currently claims
only state/beliefs.md and state/open_questions.md are new but the PR also adds
state/plan.md, state/checkpoint.md, state/gate.md and state/LEDGER.md, so update
D009 (D009.1/D009.2) to explicitly classify each file as either a standing alias
(e.g., DECISIONS.md, ROADMAP.md, NEXT.md mappings) or a per-GOAL/runtime state
file (list the added files state/plan.md, state/checkpoint.md, state/gate.md,
state/LEDGER.md) and ensure ORCHESTRATOR.md and CLAUDE.md pointer text reflects
that separation; mention the belief layer (state/beliefs.md) remains
single-writer and subagent deltas are proposals so the ratified contract matches
the actual files landed.
| - **Joe-clears-dedup-manually**: rejected — Rule 36 violation (forces a decision on Joe). | ||
|
|
||
| **Related.** | ||
| - [D006](#d006--delivery-channel-dedicated-hanna-icloud-calendar-with-0-minute-anchor-events) + [D010](#d010--l4b-rhythm-anchor-events-land-at-0900-et-phase-anchor-not-compose-moment). |
There was a problem hiding this comment.
Fix the broken D010 cross-reference.
markdownlint is flagging the fragment on Line 617 as invalid, so this "Related" link won't navigate. Please update the fragment or replace it with a plain section reference.
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)
[warning] 617-617: Link fragments should be valid
(MD051, link-fragments)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/DECISIONS.md` at line 617, The D010 cross-reference fragment is invalid
and breaking markdown navigation; update the link target for D010 in the Related
list so it matches the actual generated anchor or replace it with a plain
section reference: either correct the fragment to the exact slugified header for
"D010 -- l4b rhythm anchor events land at 0900 et phase anchor not compose
moment" (matching how your markdown processor lowercases, removes punctuation
and spaces) or change the link text to "D010" followed by the full section title
as plain text so the reference works without a fragment.
| | c009 | ORCHESTRATOR.md §7 forbids standing files under `state/` other than beliefs.md + open_questions.md, yet `state/plan.md` exists today as a standing file holding the live GOAL — the orchestrator self-violates its own adapter rule on its first run | 0.9 | ORCHESTRATOR.md:293,297,306-308 + the live `state/plan.md` file. **Cross-verified by scout-architecture AND scout-docs independently.** | none | c015 | superseded | 2026-05-25 | 2026-05-25 | | ||
| | c010 | D006's Calendar-channel choice is platform-coupled to macOS (osascript) and no D-entry yet resolves non-mac behavior; L4b will hit this on first dispatch | 0.85 | DECISIONS.md:307,325 + ROADMAP.md:247,256,269 + BLUEPRINT.md:199 + scout-architecture + scout-lanes-schemas | none | none | active | 2026-05-25 | 2026-05-25 | | ||
| | c011 | Hanna's "always-on" README claim is operationally unbacked: zero logging in src/, no scheduler/cron/launchd/systemd/.plist, bin/hanna-brief.command opens a static HTML mockup and does not invoke Python, no health probe | 0.95 | scout-ops empirical grep for `import logging` = 0 hits; no scheduler artifacts in repo; bin/hanna-brief.command:36 reads `open "$BRIEF_PATH"`; README.md:7 self-claims always-on | none | c027 | superseded | 2026-05-25 | 2026-05-25 | | ||
| | c012 | BriefPayload + briefs SQLite schema have zero idempotency token; L4b `publish(brief: BriefPayload) -> CalendarEventId` would create duplicate Joe-visible calendar events on every retry | 0.9 | scout-ops + scout-lanes-schemas: briefs schema has AUTOINCREMENT id, no UNIQUE constraint; BriefPayload has no dedup field; no event-id-lookup in scripts/first_hanna_brief.py | none | none | active | 2026-05-25 | 2026-05-25 | |
There was a problem hiding this comment.
Fix broken supersession chain between c012 and c017.
c017 (line 27) declares SUPERSEDES: c012, but c012 (line 22) shows SUPERSEDED_BY: none and STATUS: active. This violates the single-writer belief integrity contract from ORCHESTRATOR.md (line 153: "Mark superseded with explicit link").
Additionally, c017's EVIDENCE field says "Strengthens c012" but c012 and c017 are semantically contradictory (c012 claims no idempotency exists, c017 claims idempotency is confirmed), so "strengthens" is incorrect—this is a supersession, not a refinement.
🔧 Proposed fix
Update c012 (line 22) to reflect the supersession:
-| c012 | BriefPayload + briefs SQLite schema have zero idempotency token; L4b `publish(brief: BriefPayload) -> CalendarEventId` would create duplicate Joe-visible calendar events on every retry | 0.9 | scout-ops + scout-lanes-schemas: briefs schema has AUTOINCREMENT id, no UNIQUE constraint; BriefPayload has no dedup field; no event-id-lookup in scripts/first_hanna_brief.py | none | none | active | 2026-05-25 | 2026-05-25 |
+| c012 | BriefPayload + briefs SQLite schema have zero idempotency token; L4b `publish(brief: BriefPayload) -> CalendarEventId` would create duplicate Joe-visible calendar events on every retry | 0.9 | scout-ops + scout-lanes-schemas: briefs schema has AUTOINCREMENT id, no UNIQUE constraint; BriefPayload has no dedup field; no event-id-lookup in scripts/first_hanna_brief.py | none | c017 | superseded | 2026-05-25 | 2026-05-25 |And correct c017's EVIDENCE wording:
-| c017 | D012 idempotency contract is empirically on-disk-confirmed: brief_id is deterministic SHA256[:16] of (phase, anchor_date, sorted products); briefs SQLite UNIQUE on brief_id + `INSERT OR IGNORE` → double-`_persist` of same composition yields 1 row | 0.92 | tests/test_first_hanna_brief.py::TestPersistIdempotency.test_double_persist_yields_single_row passes; tests/test_schemas.py::TestComputeBriefId verifies determinism + sort-normalization; closes q006. Strengthens c012. | c012 | none | active | 2026-05-25 | 2026-05-25 |
+| c017 | D012 idempotency contract is empirically on-disk-confirmed: brief_id is deterministic SHA256[:16] of (phase, anchor_date, sorted products); briefs SQLite UNIQUE on brief_id + `INSERT OR IGNORE` → double-`_persist` of same composition yields 1 row | 0.92 | tests/test_first_hanna_brief.py::TestPersistIdempotency.test_double_persist_yields_single_row passes; tests/test_schemas.py::TestComputeBriefId verifies determinism + sort-normalization; closes q006. Supersedes c012. | c012 | none | active | 2026-05-25 | 2026-05-25 |Also applies to: 27-27
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@state/beliefs.md` at line 22, The belief entry c017 declares SUPERSEDES: c012
but c012 still shows SUPERSEDED_BY: none and STATUS: active, and c017's EVIDENCE
incorrectly says "Strengthens c012"; update c012 to set SUPERSEDED_BY: c017 and
change STATUS to superseded (or equivalent per ORCHESTRATOR.md), and edit c017's
EVIDENCE to indicate it supersedes/replaces c012 (e.g., "Supersedes c012:
confirms idempotency") so the supersession chain and wording are consistent with
the single-writer integrity contract; ensure you modify the fields named
SUPERSEDED_BY, STATUS in the c012 entry and the EVIDENCE field in the c017
entry.
| - state/beliefs.md belief deltas: met — c006–c030 added/superseded across phases; single-writer rule honored (orchestrator-only) | ||
| - state/open_questions.md updates: met — q001–q013 tracked; q003 + q004 + q005 + q006 + q009 + q010 + q013 closed by phase work | ||
| - Joe presented + "go" ask: met (turn N-7); Joe responded "Go on everything!" — implementation scope expanded to all 6 phases | ||
| - Implementation cycle Phase 0–6 complete: met — 7 commits (one per phase + the synthesis checkpoint that preceded) |
There was a problem hiding this comment.
Fix the cycle commit count.
Line 10 says the cycle completed in 7 commits, but the receipt list below contains 8 entries: the checkpoint, Phases 0–5, and this Phase 6/EXIT commit. Since this file is the durable EXIT receipt, that mismatch makes the audit trail ambiguous.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@state/gate.md` at line 10, Update the commit count statement in the durable
EXIT receipt: change the text "Implementation cycle Phase 0–6 complete: met — 7
commits (one per phase + the synthesis checkpoint that preceded)" to reflect
eight commits (e.g. "… — 8 commits") so the numeric total matches the receipt
list; ensure the parenthetical clarification correctly indicates the checkpoint
plus the phases and the final EXIT commit.
|
|
||
| | name | goal | approach | verifier-result | when-to-use | when-not | cross-ref | | ||
| |---|---|---|---|---|---|---| | ||
| | canonical-commit-trailer | every commit ends with the same trailer for impersonation-classifier safety | `🤖 Generated with [Claude Code]...\n\nCo-Authored-By: Claude <[email protected]>` | landed across 35+ commits with no classifier hits | every commit | when the user explicitly says "drop the trailer" | CLAUDE.md §"Commit trailer (canonical)" | |
There was a problem hiding this comment.
Remove the "drop the trailer" exception.
This row teaches future runs to omit the canonical commit trailer when asked, but repo policy says every commit must carry that trailer. Keeping the exception here will cause orchestrator-guided work to drift out of compliance.
Based on learnings: Every commit must end with the canonical Claude Code trailer: 🤖 Generated with [Claude Code](https://claude.com/claude-code) followed by Co-Authored-By: Claude <noreplyanthropic.com>.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@state/LEDGER.md` at line 9, Remove the exception that allows omitting the
canonical commit trailer in the ledger row identified by
canonical-commit-trailer: delete the "when the user explicitly says 'drop the
trailer'" clause so the rule always requires appending the canonical Claude Code
trailer (`🤖 Generated with [Claude Code](https://claude.com/claude-code)`
followed by `Co-Authored-By: Claude <noreplyanthropic.com>`) to every commit;
ensure the row's condition column only reflects that the trailer is mandatory
and update any related explanatory text referencing canonical-commit-trailer to
match the mandatory policy.
| def test_two_frames_in_one_read_both_decoded(self) -> None: | ||
| proc = _PipeProc() | ||
| try: | ||
| bridge = HarloBridge() | ||
| payload_one = {"jsonrpc": "2.0", "id": 1, "result": {"step": "first"}} | ||
| payload_two = {"jsonrpc": "2.0", "id": 2, "result": {"step": "second"}} | ||
| # Write BOTH frames in a single os.write — the read side will | ||
| # very likely receive them in one os.read(), exercising the | ||
| # coalescing path. | ||
| proc.feed(_frame(payload_one) + _frame(payload_two)) | ||
| first = bridge._read_frame_with_timeout(proc, timeout=1.0) # type: ignore[arg-type] | ||
| assert first == payload_one | ||
| # The second call must NOT block on the pipe (the bytes are | ||
| # already in _recv_buffer); a generous timeout protects CI but | ||
| # the call should return immediately on the happy path. | ||
| second = bridge._read_frame_with_timeout(proc, timeout=1.0) # type: ignore[arg-type] | ||
| assert second == payload_two | ||
| # After consuming both frames cleanly, the buffer is drained. | ||
| assert len(bridge._recv_buffer) == 0 | ||
| finally: | ||
| proc.close() | ||
|
|
||
| def test_partial_second_frame_buffered(self) -> None: | ||
| proc = _PipeProc() | ||
| try: | ||
| bridge = HarloBridge() | ||
| payload_one = {"jsonrpc": "2.0", "id": 10, "result": {"ok": True}} | ||
| payload_two = {"jsonrpc": "2.0", "id": 11, "result": {"ok": "again"}} | ||
| frame_two = _frame(payload_two) | ||
| # Frame-1 complete, then only the header block of frame-2 (split | ||
| # at the body boundary so phase-2 has bytes but phase-3 must | ||
| # wait for more). | ||
| header_end = frame_two.index(b"\r\n\r\n") + 4 | ||
| head_only = frame_two[:header_end] | ||
| tail = frame_two[header_end:] | ||
| proc.feed(_frame(payload_one) + head_only) | ||
| first = bridge._read_frame_with_timeout(proc, timeout=1.0) # type: ignore[arg-type] | ||
| assert first == payload_one | ||
| # Buffer must hold the partial frame-2 header bytes for the next | ||
| # call — proves the coalesce path didn't discard them. | ||
| assert len(bridge._recv_buffer) > 0 | ||
| assert bytes(bridge._recv_buffer) == head_only | ||
| # Feed the remaining body and complete the second read. | ||
| proc.feed(tail) | ||
| second = bridge._read_frame_with_timeout(proc, timeout=1.0) # type: ignore[arg-type] | ||
| assert second == payload_two | ||
| assert len(bridge._recv_buffer) == 0 | ||
| finally: | ||
| proc.close() | ||
|
|
||
| def test_recv_buffer_cleared_on_close(self) -> None: | ||
| proc = _PipeProc() | ||
| try: | ||
| bridge = HarloBridge() | ||
| payload_one = {"jsonrpc": "2.0", "id": 20, "result": {"ok": True}} | ||
| payload_two = {"jsonrpc": "2.0", "id": 21, "result": {"ok": True}} | ||
| frame_two = _frame(payload_two) | ||
| # Frame-1 complete + partial frame-2 (header only) so that after | ||
| # reading frame-1, _recv_buffer has leftover bytes. | ||
| header_end = frame_two.index(b"\r\n\r\n") + 4 | ||
| proc.feed(_frame(payload_one) + frame_two[:header_end]) | ||
| first = bridge._read_frame_with_timeout(proc, timeout=1.0) # type: ignore[arg-type] | ||
| assert first == payload_one | ||
| assert len(bridge._recv_buffer) > 0 | ||
| # close() must drain the buffer so a fresh proc handle doesn't | ||
| # inherit stale bytes from a previous subprocess generation. | ||
| bridge.close() | ||
| assert len(bridge._recv_buffer) == 0 | ||
| finally: | ||
| proc.close() |
There was a problem hiding this comment.
Make the coalescing path deterministic.
These tests rely on one os.write() being observed with the exact read boundaries needed to populate _recv_buffer. Pipes do not guarantee that, so CI can pass or fail without actually exercising the regression path. Please force the chunking explicitly for the target fd (for example by mocking the read primitive) instead of depending on kernel pipe behavior.
| def test_status_subprocess_returns_valid_json_with_expected_keys(self): | ||
| # Run the status probe in a subprocess so we exercise the real | ||
| # __main__ entrypoint (not a function call). harlo binary is absent | ||
| # in this environment → harlo_reachable=false is the expected path. | ||
| result = subprocess.run( | ||
| [sys.executable, "-m", "src", "status"], | ||
| cwd=str(_REPO_ROOT), | ||
| capture_output=True, | ||
| text=True, | ||
| timeout=15, | ||
| ) | ||
| assert result.returncode == 0, ( | ||
| f"non-zero exit\nstdout={result.stdout!r}\nstderr={result.stderr!r}" | ||
| ) | ||
| # JSON on stdout; logs on stderr. | ||
| payload = json.loads(result.stdout.strip()) | ||
| for key in ( | ||
| "hanna", | ||
| "harlo_reachable", | ||
| "harlo_burnout", | ||
| "ts", | ||
| "phase", | ||
| "next_phase_boundary_eta_minutes", | ||
| ): | ||
| assert key in payload, f"missing key {key!r} in {payload!r}" | ||
| assert payload["hanna"] == "ok" | ||
| assert isinstance(payload["harlo_reachable"], bool) | ||
| # harlo binary absent in CI → unreachable is the operational path. | ||
| assert payload["harlo_reachable"] is False | ||
| assert payload["harlo_burnout"] is None |
There was a problem hiding this comment.
Make this subprocess expectation independent of the host environment.
This only passes when harlo is missing from PATH. On any machine where the binary exists, payload["harlo_reachable"] can legitimately be True and the test will fail for the wrong reason. Pass a controlled env with a known-empty/bogus PATH (or stub harlo) so the unreachable branch is deterministic.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/test_log.py` around lines 65 - 94, The test assumes harlo is absent
from PATH; make the subprocess deterministic by passing an explicit env to
subprocess.run in test_status_subprocess_returns_valid_json_with_expected_keys:
copy os.environ, set env["PATH"] to a known-empty or bogus path (e.g., "" or a
tmp dir) so the harlo lookup always fails, then pass that env to subprocess.run;
update the call in the test function (the subprocess.run invocation) to include
env=env (and preserve other necessary env vars from os.environ.copy()) so the
unreachable branch is deterministic.
…on the FORUM; Line A dispatched
Per harness DELIBERATE phase (Analyst + Critic lead): 5 candidate
proposals reviewed; 4 survive critique-before-build; 1 died (Line C
spike); Line B queue resequenced (OverrideToken + Formation schemas
deferred).
Ratified main-thread per D002 (decisions author-by-main-thread):
D014 — LockoutResponse shape (closes q002).
Structured no-op JSON returned (not raised) from MCP tools during
FAMILY_LOCKOUT, with `paused: true` boolean at top + reason/phase/
next_anchor_iso/message/override_path_hint fields. Rejected:
exception-with-rich-message (Rule 36 risk + Claude Code renders as
error); decorator-injected-skip (opaque); exception-with-structured-
payload (conflates success/failure). Belief c031 records this.
D015 — Brief composition boundary (closes q007).
One composition = one MCP-tool body invocation. begin_composition at
tool entry; end_composition at tool exit (try/finally); transitively
any Harlo `coach` call inside the tool body shares the same
composition; nested compositions raise HarloCompositionAlreadyActive.
Rejected: composition = one brief artifact (too narrow for
hanna_log); composition = one MCP session (too coarse, defeats D001
rate-limit); composition = one Harlo subprocess lifetime (defeats
L3b reuse). Belief c032 records this.
DECISIONS.md footer: D014 -> D016 (next number).
Line C — DIED on the FORUM.
The proposal "critic[evaluate] dispatches to assess Octavius
reachability via subprocess.Popen spike" died because the Linux
sandbox cannot run macOS-only binaries; the L1 verifier is
inaccessible from this env. Burning an agent would return "I don't
know" — no information delta from today. Replacement: free-information
AskUserQuestion to Joe (separate UI surface, this turn).
DEADENDS.md records the generalized class:
axis: external-system reachability verification
direction: Linux-sandbox subprocess spike for macOS-only binaries
rejection: the agent cannot reach the target; ask the human who has
access — q016 carries the question.
Belief c033 records the dead-end class.
Line B reorganization.
OverrideToken schema deferred (q014 secret-storage substrate is
unresolved — committing fields prematurely risks redo).
FormationRequest/FormationOutput deferred (Line C / Octavius
reachability gates the contract shape).
Surviving Line B cycle-1 work: D014 (done) + D015 (done) +
JoeStateSnapshot (next cycle worker dispatch).
Line A — Dispatched.
Worker brief loaded with R1-R5 mitigations from the FORUM critique:
AppleScript escape via quoted form + tempfile body; documented
Calendar.app permissions path; HannaCalendarPermissionRequired
exception; calendar-identity parameterization; archive raises
HannaCalendarNotFound cleanly without auto-create. Worker is in
flight async; will return with src/channels/{__init__,calendar}.py,
4 sibling .plists, briefs SQLite reconciliation columns,
tests/test_calendar.py + tests/test_reconciliation.py.
State updates:
- state/beliefs.md: c031 (D014 ratified), c032 (D015 ratified),
c033 (dead-end class recorded).
- state/open_questions.md: q002 closed by c031; q007 closed by c032;
q014 (secret-storage substrate) opened; q015 (HMAC rotation) opened;
q016 (Octavius reachability — replacement for the dead Line C spike)
opened.
Harness artifacts updated:
- state/tasks/hanna-real-arc/FORUM.md: cycle 1 deliberation log with
per-proposal critique + severity x likelihood ratings + result.
- state/tasks/hanna-real-arc/DEADENDS.md: cycle-1 dead-end class.
- state/tasks/hanna-real-arc/TRACE.md: gate-cross + critique-result +
reorganize events.
- state/tasks/hanna-real-arc/LOG.md: cycle-1 Line C dead-end row.
Budget posture: cycle 1 spent so far ~0 agent dispatches finished;
1 worker in flight (Line A). 49 agents remain in the 50-cap arc budget.
No code/tests changed by this commit. Worker's edits land separately.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <[email protected]>
…4 sibling launchd .plists + tests
Cycle-1 Line A delivery from the harness PLAN.md. Worker dispatched
per the FORUM-survived proposal (rating 4/5; R1–R5 mitigations baked
in). Critic[verify] is a separate dispatch (next).
Created:
- src/channels/__init__.py — package init.
- src/channels/calendar.py — D006 Calendar channel implementation.
publish(brief: BriefPayload, calendar_name="Hanna") -> CalendarEventId | None
Honors D010 (uses brief.phase_anchor_iso as event start),
D011 (HannaCalendarNotAvailable on non-mac), D012 (idempotent
lookup by brief_id), D014 (returns None on FAMILY_LOCKOUT).
AppleScript via osascript with R1 mitigation: body written to
tempfile + read via `do shell script "cat " & quoted form`
(escape-safe). R2 mitigation: HannaCalendarPermissionRequired
raised on osascript -1743 with grant instructions in message.
R3 mitigation: calendar_name parameterized with "Hanna"
default; duplicate detection via summary-contains query.
R5 mitigation: archive() raises HannaCalendarNotFound cleanly
on missing "Hanna · Archive".
archive(event_id, archive_calendar_name="Hanna · Archive") -> None
Exception hierarchy: HannaCalendarError + 4 subclasses.
`python3 -m src.channels.calendar publish-now` entry point per
ROADMAP §4 L4b Phase-2 swap target.
Logging: src._log.get_logger("hanna.calendar") wired at start +
success (INFO) + failure (WARNING) + idempotency hit (DEBUG).
- bin/com.hanna.brief.midday.plist — Mon-Fri 12:00 ET (D010 anchor).
- bin/com.hanna.brief.evening.plist — Mon-Fri 17:00 ET.
- bin/com.hanna.brief.weekly_monday.plist — Mon 09:30 ET.
- bin/com.hanna.brief.weekly_friday.plist — Fri 16:00 ET.
- tests/test_calendar.py — mocked-subprocess tests covering each
publish/archive branch + the 4 exception classes.
- tests/test_reconciliation.py — property tests on the
`calendar_event_uid XOR unpublished_reason` invariant + the 3
unpublished_reason path tests.
Modified:
- src/schemas.py — CalendarEventId NewType added at line 19 per
D006/D012 contract.
- scripts/first_hanna_brief.py — SCHEMA gains `calendar_event_uid TEXT`
+ `unpublished_reason TEXT` columns; _persist now applies ALTER
TABLE migration idempotently (try/except sqlite3.OperationalError
for the column-already-exists case); main() wires publish() after
_persist with try/except HannaCalendarNotAvailable + generic
HannaCalendarError branches; updates the SQLite row with
calendar_event_uid on success or unpublished_reason on graceful
failure ("non_macos" / "publish_failed" / "family_lockout").
- bin/README.md — documents the Calendar.app permissions grant path
(System Settings → Privacy & Security → Automation), the manual
creation of "Hanna" and "Hanna · Archive" iCloud calendars
(NOT auto-created per D011), and the 4 sibling .plist install
commands (launchctl load).
CONTRACT served (per PLAN.md Line A):
- P2 (calendar publish at anchor + idempotency end-to-end on Joe's
Mac): mocked-subprocess L1 + L2 verifiers pass; L3+L4 require
Joe's Mac (stochastic per SPEC).
- P6 (reconciliation invariant): briefs row gains the two columns;
invariant test passes in tests/test_reconciliation.py.
- P9 partial (launchd-side restart survival): 4 sibling .plists
authored matching D010's full anchor table.
Verifier results (L0+L1+L2 — L3/L4 deferred to Joe's Mac):
- python3 -m pytest tests/ -q → 139 passed in 0.89s
(117 baseline + 22 new across test_calendar + test_reconciliation
+ Line A's incremental coverage of existing modules).
- L0: src/channels imports clean; all .plist files parse as XML.
- L1: mocked-subprocess tests cover every publish/archive branch +
each exception class.
- L2: idempotency property holds; reconciliation XOR-invariant holds.
DEADENDS check pre-build: none touched.
LEDGER consultation: `moe-dispatch-d002` (this IS a worker dispatch);
`pytest-as-l1` (used as L1 verifier).
Trailer hygiene clean: all new files are fresh seeds (no Harlo
ancestor) → no attribution trailer. Existing files keep their state.
No model-id strings. Stdlib-only.
State updates pending (deferred to critic[verify] cycle):
- CHAMPION.md: predicate-score delta (P2: 0.80→0.95, P6: 0.75→0.80,
P9: 0.65→0.70) — write after critic[verify] PASS confirms.
- LOG.md: cycle-1 Line A row with the delivery summary.
- TRACE.md: branch-decision + verifier-result events.
Next dispatch: critic[verify] on Line A against the FORUM-rated
acceptance criteria; on PASS, champion promotes from 0.27 to
~0.42 (5 predicate-fractions advanced).
Q016 (Octavius reachability) still awaiting Joe's answer — does not
gate this commit.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <[email protected]>
…s seed-2118024 → arc-cycle1-line-A-a2d64cd (0.27 → 0.435); D016 ratified (Octavius exists on GH, not local); q016 closed, q017 opens Per harness cycle 1 EXECUTE phase (Builder leads; Critic on call): 1. Line A delivery (commit a2d64cd) verified PASS 36/36 by critic[verify] against FORUM-rated acceptance criteria. All R1–R5 mitigations visibly present in code; 139/139 tests; L0+L1+L2 verifiers green; L3+L4 require Joe's Mac (stochastic per SPEC). 2. CHAMPION promoted per OP6 promotion rule (strict increase ≥1 predicate; no regression): - P2 (calendar publish + idempotency): 0.40 → 0.85 (publish+archive lands; mocked-subprocess tests cover every branch + exception) - P5 (LockoutResponse on lockout): 0.20 → 0.50 (D014 ratifies shape) - P6 (reconciliation invariant): 0.10 → 0.70 (columns + XOR test pass at L2) - P7 (≥200 tests): 0.59 → 0.70 (139/200) - P9 (restart survival): 0.30 → 0.50 (4 sibling .plists land) - Champion score: 0.27 → 0.435. 3. Joe's direct reply to q016 — "It exists on GitHub not locally yet" — closes q016. F3 (Octavius doesn't exist) is FALSE; L7 IS buildable in principle, but two preconditions: (a) Joe installs Octavius locally for L7's runtime verifier (P4). (b) Hanna learns Octavius's IPC contract — raised as q017 with three paths: (i) Joe pastes the contract verbatim; (ii) widen mcp__github__* scope to include Octavius; (iii) Joe installs + runs once + shares the API shape from real traffic. 4. D016 ratified main-thread per D002 (decisions author-by-main-thread): - L7 `src/octavius_bridge.py` lands as a NotImplementedError stub until q017 closes; FormationRequest/Output schemas defer. - L6's `hanna_formation_request` returns a LockoutResponse-equivalent `{ "octavius_installed": false, ... }` JSON until Joe's local install completes. - Arc CAN terminate at "Hanna is real with Octavius stubbed" if q017 stays open at SHIP; documented bounded weakness, not a showstopper. - DEADENDS-c033 class extends (c035): Linux-sandbox + restricted GH MCP scope can't reach external resources outside that scope. 5. State updates (per ORCHESTRATOR.md Invariant 2): - state/beliefs.md: c034 (Octavius exists on GH, not local), c035 (DEADENDS class extension). - state/open_questions.md: q016 closed by c034; q017 (contract surfacing path) opens. - docs/DECISIONS.md: D016 ratified; footer D016 → D017. 6. Harness artifacts updated: - state/tasks/hanna-real-arc/CHAMPION.md: full rewrite at the post-promotion state; predicate-by-predicate score delta; reproduction recipe; gap-to-"Hanna is real" summary. - state/tasks/hanna-real-arc/LOG.md: cycle 1 Line A PROMOTE row + Line B substrate-decision PROMOTE + Line C-replacement INFORMATION. - state/tasks/hanna-real-arc/TRACE.md: 5 cycle-1 EXECUTE events (external-call to Joe; D016 branch-decision; 2 verifier-results; RECOMMENDED-CHANGES park). - state/tasks/hanna-real-arc/DEADENDS.md: c035 class generalization. 3 RECOMMENDED-CHANGES from critic[verify] parked (cosmetic): - json import scope in src/channels/calendar.py (cosmetic). - AppleScript date constructor locale dependency (English-only %A/%B in _format_applescript_date). - _RECONCILIATION_MIGRATIONS runs on every _persist call (silently absorbs duplicate-column OperationalError; cheap, but a PRAGMA table_info precheck would be tidier). Budget posture: cycle 1 spent ~3 agent dispatches (Line A worker + Line A critic[verify] + the dead Line C spike replaced by free AskUserQuestion). 47 agents remain in 50-cap arc budget. Cycle 2 entering: REORGANIZE — re-derive lines from CHAMPION state. Candidates: - Line B Phase 2: JoeStateSnapshot schema authoring (deferred from cycle 1). - Line D opens (was held): L6 mcp_tools partial — author the 5 read-shaped tools (status / morning / midday / evening / weekly_*) that don't depend on Octavius; defer hanna_formation_request. - New Line F: L7 octavius_bridge.py stub authoring per D016. - q017 resolution: AskUserQuestion to Joe re contract-surfacing path (free info; no agent cost). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
…ion; Lines B-cont/D/F open
Per harness REORGANIZE phase at cycle boundary:
q017 closed by Joe's reply (c) "install + share traffic":
- closed-pending-action state — Joe's manual install is async; Hanna
proceeds on lines that don't depend on Octavius's actual contract.
- belief c036 records the path choice and the gating relationship.
Cycle 1 receipts:
- Line A: DELIVERED + PROMOTED (champion arc-cycle1-line-A-a2d64cd;
critic[verify] PASS 36/36; champion score 0.27 → 0.435).
- Line B: PARTIAL — D014 + D015 ratified (q002 + q007 closed);
JoeStateSnapshot deferred to cycle 2; Override + Formation deferred
farther pending q014 + q017.
- Line C: CLOSED — died on FORUM, replaced by Joe's free-info reply
via AskUserQuestion → D016 ratified; q016 closed; q017 raised then
closed-pending-action.
- belief c037 records cycle 1 closure.
Cycle 2 line structure (PLAN.md updated):
- Line B-cont: JoeStateSnapshot schema authoring (deferred from
cycle 1; light worker; rating 3/5).
- Line D (OPENED — was held): L6 mcp_tools partial — 9 of 10
hanna_* tools that don't depend on Octavius;
hanna_formation_request returns a D014-shaped
`{available: false, reason: "OCTAVIUS_NOT_INSTALLED", ...}` stub
until q017's pending-action completes (rating 5/5).
- Line F (NEW): L7 octavius_bridge.py stub authoring per D016 —
OctaviusNotInstalled exception (NOT bare NotImplementedError) +
mocked-subprocess tests against the BLUEPRINT §9 stub shape
(rating 2/5).
- Line G (CONSIDERED, DEFERRED): q014 secret-storage substrate
D-entry for OverrideToken — high-leverage but not arc-terminal;
defer to cycle 3 unless a forcing function surfaces.
- Lines A, C: RETIRED.
- Line E (INTEGRATE+STRESS+SHIP): still held — gates on D ship +
Joe's install step.
Cycle 2 critique-before-build (FORUM):
- Line D: R1 (composer-extraction refactor risk), R2 (composition
state leak on tool error), R3 (formation_request stub renders as
error — F6-class), R4 (APPEND-ONLY race with Joe's editor). All
mitigations folded into the upcoming worker brief.
- Line B-cont: cycle-1 critique stands; `extra` field for
forward-compat.
- Line F: R1 (use OctaviusNotInstalled, not bare NotImplementedError),
R2 (BLUEPRINT §9 as stub source-of-truth; real-shape lands
post-q017).
Mode re-derived: ORCHESTRATED TEAM preserved (BREADTH=3,
INDEPENDENCE=medium, HORIZON=long, REWORK COST=high,
VERIFIER COST=medium). HYSTERESIS rule honored.
Suggested cycle 2 dispatch ordering: B-cont + F in parallel (small,
disjoint files); D as the largest single dispatch; critic[verify]
over all three at the end. Budget estimate: ~7 agents
(3 workers + 1 critic[verify] x 3 with possible integrator if
parallel writes conflict).
Budget posture: cycle 1 spent ~3 agents (Line A worker + Line A
critic[verify] + the dead Line C spike replaced by free
AskUserQuestion + main-thread D014/D015/D016 ratifications). 47
agents remain in 50-cap arc budget; cycle 2 estimate keeps 40+
in reserve.
State updates:
- state/beliefs.md: c036 (q017 closed-pending-action), c037
(cycle 1 closed).
- state/open_questions.md: q017 status → closed-pending-action.
- state/tasks/hanna-real-arc/PLAN.md: cycle 2 reorganization;
3 lines open + 1 considered+deferred; per-line ranked queues;
REORGANIZE triggers carried.
- state/tasks/hanna-real-arc/LOG.md: cycle 1 close row + cycle 2
open row.
- state/tasks/hanna-real-arc/TRACE.md: 6 cycle-boundary events.
Cycle 2 enters DELIBERATE-complete → EXECUTE-ready state. Awaiting
go-or-redirect signal to dispatch the 3 worker briefs (B-cont, D, F).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <[email protected]>
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (2)
tests/test_reconciliation.py (1)
116-127: 💤 Low valueConsider extracting duplicated _StubBridge class.
The
_StubBridgeclass is defined identically in five test methods. While this doesn't affect correctness, extracting it to a pytest fixture or module-level helper would reduce duplication and make future updates easier.Example refactor
+# Module-level test helper +class _StubBridge: + def __enter__(self): + return self + + def __exit__(self, exc_type, exc, tb): + return None + + def drive_coaching_exchange(self): + from src.harlo_bridge import HarloUnreachable + raise HarloUnreachable("stubbed") + + class TestReconciliationColumns: def test_successful_publish_records_uid_and_clears_reason( self, monkeypatch, tmp_path ): # ... - class _StubBridge: - # ... (remove) - monkeypatch.setattr(_module, "HarloBridge", _StubBridge)Also applies to: 203-214, 243-254, 290-301, 352-363
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/test_reconciliation.py` around lines 116 - 127, The _StubBridge class is duplicated across multiple tests; extract it into a single reusable definition (either a module-level helper or a pytest fixture) and update tests to use that shared provider instead of redefining _StubBridge in each test; ensure the shared helper implements __enter__, __exit__, and drive_coaching_exchange (raising src.harlo_bridge.HarloUnreachable("stubbed")) so calls in the tests that reference _StubBridge and drive_coaching_exchange keep the same behavior.src/channels/calendar.py (1)
100-122: ⚖️ Poor tradeoffFix locale-dependent AppleScript
date "<string>"parsing in calendar publish
src/channels/calendar.py::_format_applescript_dateformats a free-form, human-readable string (weekday/month from Pythonstrftimeplus hardcoded tokens like"at"and"AM"/"PM") and then relies on AppleScriptdate "<string>"to parse it. AppleScript string-to-date coercion is driven by the Mac’s Language & Region / Date settings, so this can fail on non-matching locales.Use a locale-independent approach instead—e.g., pass an ISO-8601 value (
as «class isot») or construct the AppleScript date by assigning numeric fields (year/month/day/hours/minutes/seconds).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/channels/calendar.py` around lines 100 - 122, The current _format_applescript_date builds a locale-dependent human string; replace it with a locale-independent AppleScript date literal by returning an ISO-8601-based expression (use AppleScript's ISO literal or numeric fields). Update _format_applescript_date to stop using strftime pieces and instead return an AppleScript-compatible ISO expression such as using «class isot» with the original anchor_iso (or build a numeric field constructor with year, month, day, hour, minute, second) so AppleScript parsing is independent of system locale; keep the function name _format_applescript_date and ensure the returned string is the AppleScript snippet you will inject into the script.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/channels/calendar.py`:
- Around line 339-364: The temp file created for the brief body
(body_file/body_path) is never removed; update the publish flow around the
_build_publish_script/_run_osascript calls to always unlink the tempfile in a
finally block (remove the file on both success and error paths, including after
raising _interpret_failure), and move/add the os import to the module top so
os.unlink can be used; reference body_file/body_path, _build_publish_script,
_run_osascript, and _interpret_failure when making the change.
In `@state/open_questions.md`:
- Line 14: The open question q005 is marked "closed" despite its closing claim
c018 in beliefs.md having only 0.7 confidence and saying it "closes q005
conditionally"; update state/open_questions.md so q005 is not fully closed:
change its status from "closed" to a more accurate state such as
"closed-pending-action" (matching q017's pattern) or reopen it until c018's
confidence is raised to ≥0.8 or the L4b end-to-end path is exercised; make the
change where q005 appears in state/open_questions.md and ensure the status
aligns with the closure rule (closure requires confidence ≥0.8) and with c018's
conditional wording.
In `@tests/test_reconciliation.py`:
- Around line 161-194: The test mixes SQL NULL and empty-string for lockout
briefs; make it consistent with the D012 convention and the _lockout_brief()
helper which uses brief_id == "" by updating the manual INSERT to set brief_id
to "" (empty string) instead of None and change the SELECT to query WHERE
brief_id = "" (or otherwise ensure _lockout_brief/_update_reconciliation
behavior is aligned); keep the call to _update_reconciliation(brief_id="") as-is
and adjust the INSERT/SELECT to match that empty-string semantics.
---
Nitpick comments:
In `@src/channels/calendar.py`:
- Around line 100-122: The current _format_applescript_date builds a
locale-dependent human string; replace it with a locale-independent AppleScript
date literal by returning an ISO-8601-based expression (use AppleScript's ISO
literal or numeric fields). Update _format_applescript_date to stop using
strftime pieces and instead return an AppleScript-compatible ISO expression such
as using «class isot» with the original anchor_iso (or build a numeric field
constructor with year, month, day, hour, minute, second) so AppleScript parsing
is independent of system locale; keep the function name _format_applescript_date
and ensure the returned string is the AppleScript snippet you will inject into
the script.
In `@tests/test_reconciliation.py`:
- Around line 116-127: The _StubBridge class is duplicated across multiple
tests; extract it into a single reusable definition (either a module-level
helper or a pytest fixture) and update tests to use that shared provider instead
of redefining _StubBridge in each test; ensure the shared helper implements
__enter__, __exit__, and drive_coaching_exchange (raising
src.harlo_bridge.HarloUnreachable("stubbed")) so calls in the tests that
reference _StubBridge and drive_coaching_exchange keep the same behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 72a3f3ff-16e6-4005-bb9b-6285d4dc33cd
📒 Files selected for processing (20)
bin/README.mdbin/com.hanna.brief.evening.plistbin/com.hanna.brief.midday.plistbin/com.hanna.brief.weekly_friday.plistbin/com.hanna.brief.weekly_monday.plistdocs/DECISIONS.mdscripts/first_hanna_brief.pysrc/channels/__init__.pysrc/channels/calendar.pysrc/schemas.pystate/beliefs.mdstate/open_questions.mdstate/tasks/hanna-real-arc/CHAMPION.mdstate/tasks/hanna-real-arc/DEADENDS.mdstate/tasks/hanna-real-arc/FORUM.mdstate/tasks/hanna-real-arc/LOG.mdstate/tasks/hanna-real-arc/PLAN.mdstate/tasks/hanna-real-arc/TRACE.mdtests/test_calendar.pytests/test_reconciliation.py
✅ Files skipped from review due to trivial changes (4)
- src/channels/init.py
- bin/com.hanna.brief.weekly_monday.plist
- state/tasks/hanna-real-arc/TRACE.md
- state/tasks/hanna-real-arc/CHAMPION.md
🚧 Files skipped from review as they are similar to previous changes (2)
- src/schemas.py
- scripts/first_hanna_brief.py
| with tempfile.NamedTemporaryFile( | ||
| mode="w", | ||
| encoding="utf-8", | ||
| suffix=".md", | ||
| prefix="hanna-brief-", | ||
| delete=False, | ||
| ) as body_file: | ||
| body_file.write(brief.body_markdown) | ||
| body_path = body_file.name | ||
|
|
||
| script = _build_publish_script( | ||
| calendar_name=calendar_name, | ||
| title=title, | ||
| body_path=body_path, | ||
| anchor_applescript_date=anchor_applescript_date, | ||
| brief_id=brief.brief_id, | ||
| ) | ||
| completed = _run_osascript(script) | ||
| if completed.returncode != 0: | ||
| logger.warning( | ||
| "calendar publish failed brief_id=%s rc=%s stderr=%s", | ||
| brief.brief_id, | ||
| completed.returncode, | ||
| (completed.stderr or "").strip()[:240], | ||
| ) | ||
| raise _interpret_failure(completed, "publish") |
There was a problem hiding this comment.
Temp file leak: the brief body file is never removed.
body_file is created with delete=False and body_path is reused after the with block, but nothing unlinks it — not on success, not on the failure/exception paths. Every publish() that reaches this point leaves a hanna-brief-*.md file in the temp dir indefinitely. Clean it up in a finally.
🛠️ Proposed fix
+ import os
with tempfile.NamedTemporaryFile(
mode="w",
encoding="utf-8",
suffix=".md",
prefix="hanna-brief-",
delete=False,
) as body_file:
body_file.write(brief.body_markdown)
body_path = body_file.name
- script = _build_publish_script(
- calendar_name=calendar_name,
- title=title,
- body_path=body_path,
- anchor_applescript_date=anchor_applescript_date,
- brief_id=brief.brief_id,
- )
- completed = _run_osascript(script)
+ try:
+ script = _build_publish_script(
+ calendar_name=calendar_name,
+ title=title,
+ body_path=body_path,
+ anchor_applescript_date=anchor_applescript_date,
+ brief_id=brief.brief_id,
+ )
+ completed = _run_osascript(script)
+ finally:
+ try:
+ os.unlink(body_path)
+ except OSError:
+ passMove the import os to the module top with the other stdlib imports.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/channels/calendar.py` around lines 339 - 364, The temp file created for
the brief body (body_file/body_path) is never removed; update the publish flow
around the _build_publish_script/_run_osascript calls to always unlink the
tempfile in a finally block (remove the file on both success and error paths,
including after raising _interpret_failure), and move/add the os import to the
module top so os.unlink can be used; reference body_file/body_path,
_build_publish_script, _run_osascript, and _interpret_failure when making the
change.
| | q002 | `LockoutResponse` shape — required before the L6 `mcp_tools` lane can return structured lockout JSON (NEXT §C.4) | high | closed | c031 | 2026-05-25 | | ||
| | q003 | `docs/SESSION_01_RECON.md` §G staleness — correct the "33 rules do not exist in Harlo" claim, or stamp it as historical? (NEXT staleness flag) | low | closed | c025 | 2026-05-25 | | ||
| | q004 | Rhythm-anchor vs compose-moment — what timestamp does `BriefPayload.composed_at_iso` carry for D006 calendar publish? D006's posture rationale promises 09:00 ET anchor events but ROADMAP §4 spec hardcodes compose-moment (scout-lanes-schemas B2; blocks L4b) | high | closed | c016 | 2026-05-25 | | ||
| | q005 | D006 cross-platform stance — macOS-only with explicit `HannaCalendarNotAvailable` + platform gate, or CalDAV cross-platform path? CI runs ubuntu-latest; osascript is macOS-only (scout-lanes-schemas B1 + scout-architecture; blocks L4b) | high | closed | c018 | 2026-05-25 | |
There was a problem hiding this comment.
q005 marked closed but its closing claim is below the confidence threshold.
The closure rule on Line 3 requires the closing claim to have confidence ≥ 0.8. c018 in beliefs.md (Line 28) carries confidence 0.7 and its own evidence says it "closes q005 conditionally". Marking q005 as fully closed contradicts both the rule and c018's conditional wording. Consider closed-pending-action (as used for q017) or raise c018's confidence once the L4b end-to-end path is exercised.
📝 Proposed fix
-| q005 | D006 cross-platform stance — macOS-only with explicit `HannaCalendarNotAvailable` + platform gate, or CalDAV cross-platform path? CI runs ubuntu-latest; osascript is macOS-only (scout-lanes-schemas B1 + scout-architecture; blocks L4b) | high | closed | c018 | 2026-05-25 |
+| q005 | D006 cross-platform stance — macOS-only with explicit `HannaCalendarNotAvailable` + platform gate, or CalDAV cross-platform path? CI runs ubuntu-latest; osascript is macOS-only (scout-lanes-schemas B1 + scout-architecture; blocks L4b) | high | closed-pending-action | c018 | 2026-05-25 |📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| | q005 | D006 cross-platform stance — macOS-only with explicit `HannaCalendarNotAvailable` + platform gate, or CalDAV cross-platform path? CI runs ubuntu-latest; osascript is macOS-only (scout-lanes-schemas B1 + scout-architecture; blocks L4b) | high | closed | c018 | 2026-05-25 | | |
| | q005 | D006 cross-platform stance — macOS-only with explicit `HannaCalendarNotAvailable` + platform gate, or CalDAV cross-platform path? CI runs ubuntu-latest; osascript is macOS-only (scout-lanes-schemas B1 + scout-architecture; blocks L4b) | high | closed-pending-action | c018 | 2026-05-25 | |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@state/open_questions.md` at line 14, The open question q005 is marked
"closed" despite its closing claim c018 in beliefs.md having only 0.7 confidence
and saying it "closes q005 conditionally"; update state/open_questions.md so
q005 is not fully closed: change its status from "closed" to a more accurate
state such as "closed-pending-action" (matching q017's pattern) or reopen it
until c018's confidence is raised to ≥0.8 or the L4b end-to-end path is
exercised; make the change where q005 appears in state/open_questions.md and
ensure the status aligns with the closure rule (closure requires confidence
≥0.8) and with c018's conditional wording.
| _module._persist(_morning_brief("anchor_row"), harlo_reachable=True, db_path=db_path) | ||
| with sqlite3.connect(db_path) as conn: | ||
| _module._apply_pragmas(conn) | ||
| _module._apply_reconciliation_migrations(conn) | ||
| conn.execute( | ||
| "INSERT INTO briefs (ts, phase, body, harlo_reachable, brief_id, " | ||
| "phase_anchor_iso) VALUES (?, ?, ?, ?, ?, ?)", | ||
| ( | ||
| brief.composed_at_iso, | ||
| brief.phase.name.lower(), | ||
| brief.body_markdown, | ||
| 1, | ||
| None, | ||
| brief.phase_anchor_iso, | ||
| ), | ||
| ) | ||
| conn.commit() | ||
| # The lockout row has brief_id IS NULL; the fallback path of | ||
| # _update_reconciliation targets the most-recent NULL-brief_id row. | ||
| _module._update_reconciliation( | ||
| brief_id="", | ||
| unpublished_reason="family_lockout", | ||
| db_path=db_path, | ||
| ) | ||
| with sqlite3.connect(db_path) as conn: | ||
| rows = conn.execute( | ||
| "SELECT phase, calendar_event_uid, unpublished_reason " | ||
| "FROM briefs WHERE brief_id IS NULL" | ||
| ).fetchall() | ||
| assert len(rows) == 1 | ||
| phase, cal_uid, reason = rows[0] | ||
| assert phase == "family_lockout" | ||
| assert cal_uid is None | ||
| assert reason == "family_lockout" |
There was a problem hiding this comment.
Clarify brief_id empty-string vs NULL handling in lockout test.
The test creates a lockout brief with brief_id = None in the manual INSERT (line 173), then calls _update_reconciliation(brief_id="", ...) (line 181), then queries with WHERE brief_id IS NULL (line 188). This mixes empty string "" and SQL NULL for the same semantic concept (lockout brief has no brief_id).
According to D012 and the _lockout_brief() helper (line 52), lockout briefs use brief_id="" (empty string). The test should be consistent: either use "" throughout, or use None throughout. The current mix may work if _update_reconciliation converts "" to NULL, but that's not obvious from the test.
Suggested fix for consistency
If lockout briefs should have SQL NULL (not empty string), change line 52 in _lockout_brief() to:
- brief_id="",
+ brief_id=None,Or if they should have empty string, change the manual INSERT at line 173 and the query at line 188:
- None,
+ "",- "FROM briefs WHERE brief_id IS NULL"
+ "FROM briefs WHERE brief_id = ''"🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/test_reconciliation.py` around lines 161 - 194, The test mixes SQL NULL
and empty-string for lockout briefs; make it consistent with the D012 convention
and the _lockout_brief() helper which uses brief_id == "" by updating the manual
INSERT to set brief_id to "" (empty string) instead of None and change the
SELECT to query WHERE brief_id = "" (or otherwise ensure
_lockout_brief/_update_reconciliation behavior is aligned); keep the call to
_update_reconciliation(brief_id="") as-is and adjust the INSERT/SELECT to match
that empty-string semantics.
Summary
Two coupled deliverables on this branch:
D009— Multi-agent orchestrator framework adopted (commitc65b6ae). Generalizes Hanna's ratified D002 MoE methodology as a Claude Code primitive set:.claude/agents/{planner,worker,critic,integrator}.md; astate/durable layer with a novel belief layer (beliefs.md, supersession-tracked, single-writer); formal EXIT/HALT gates. Reuses existingdocs/DECISIONS.md/docs/ROADMAP.md §5/NEXT.mdvia an adapter — no duplicate state files./hanna-dispatch-nextbecomes one workflow under the orchestrator.First-principles improvement cycle — the orchestrator's first live GOAL. 7
critic[red_team]scouts surveyed the codebase from disjoint angles (architecture, code quality, tests, docs, ops, security/rules, lanes/schemas), surfaced 13 BLOCKER + 38 MAJOR + ~38 MINOR findings, synthesized into a PRD, presented for "go", then executed all 6 phases of the PRD's recommended cycle. 4 new D-entries (D010–D013) ratified along the way.What's new
Substrate decisions (4 new, all resolved)
HannaCalendarNotAvailablebrief_id = SHA256[:16](phase + anchor-date + sorted-products)compute_producer_phasebranch precedence: MONTHLY > WEEKLY > daily phases (documented + tested)Each entry carries explicit rejected alternatives per the orchestrator's
decisions.mdcontract.docs/DECISIONS.mdfooter nowD014.Code — 6 implementation phases, 13 agent dispatches, 13/13 critic[verify] PASS
state/plan.mdwas a standing file despite §7's "at-rest = beliefs + open_questions only" rule). §7 amended to distinguish at-rest state (durable cross-GOAL) from during-GOAL runtime state.BriefPayloadgainsphase_anchor_iso(D010) +brief_id(D012);briefsSQLite gainsUNIQUEconstraint +INSERT OR IGNORE(re-running same composition is a no-op on disk);HannaCalendarNotAvailablelazy-import-or-stub pattern (D011);TestFrameCoalescingwith 3 regression tests for the round-3_recv_bufferpatch.main()covering state-blind / FAMILY_LOCKOUT / HarloTimeout paths; sub-render coverage for_state_line/_approaching_line/_blockers_line/_compose_brief; MONTHLY-beats-WEEKLY precedence test + D013 ratification.python/hanna/+crates/— both absent); NEW Rule 35 step "enumerated bridge surface (authoritative allowlist per D001)" runs an inlinepython3block and fails if anyself._call_toolsite invokes a tool outside{status, coach, recall, query_past_experience, patterns}; Rule 37 grep extended todocs/+ last 50 commit messages; 32 non-load-bearing rules carry per-rule D008.7 annotations.RULES.md §34rewritten to "two active layers" (Layer 2 strikethrough per D008.1); README "Repository layout" regenerated against actual filesystem;SESSION_01_RECON.md §Gstamped with a> Historical noteblock;REVIEW_2026-05-22.md:4model-id string removed.src/_log.py(UTC ISO, stdliblogging, idempotent root config,HANNA_LOG_LEVELenv);src/__main__.py(python3 -m src statushealth probe emitting structured JSON); 14 lane-boundary logging sites includingHarloBridge.last_stderr()surfaced onHarloUnreachable/HarloTimeout(closes the "diagnostic data no production caller reads" gap); SQLite PRAGMA hardening (WAL+synchronous=NORMAL+busy_timeout=5000+foreign_keys=ON);bin/hanna-brief.commandswapped from "open static HTML mockup" to "invoke Python end-to-end with tee'd logging"; NEWbin/com.hanna.brief.morning.plistlaunchd schedule (Mon–Fri 09:00 ET, matches D010 MORNING anchor).ProductFile.parse()now Rule-36 faithful-surface (warns on unknown sections viahanna.schemaslogger; raisesValueErroron duplicate frontmatter keys; drops empty bullets explicitly; 64KB input cap); sub-render contract documented inscripts/first_hanna_brief.pymodule docstring; data retention outline inbin/README.md.Tests — 74 → 117 (+43 across the cycle)
_log+ status + PRAGMA tests)Belief layer — 15 new active claims, 7 superseded with explicit links
The cycle's belief-state writes (single-writer = orchestrator):
_approaching_lineheuristic is sort-by-date + cap-at-3 (clarifies docs)ProductFile.parseRule-36 faithful-surface7 superseded claims preserved with
SUPERSEDED_BYlinks per the orchestrator's "never delete" rule — the audit trail is the artifact.Open questions closed — 7 of 13
Remaining 6 carry forward (mostly medium/low leverage; two highs gate L6
mcp_tools).Commits (14)
Orchestrator install + post-merge handoff (2)
c4d621edocs(next): mark PR Hanna buildout: ratifications + harness + L1–L4a + L3b lanes #1 merged; buildout chapter closed; L4b is the next session entry pointc65b6aefeat(orchestrator): adopt multi-agent framework reusing existing Hanna state (D009)Scout phase checkpoints (4)
6938858checkpoint(orchestrator): first-principles review GOAL on disk; 7 scouts in flight3e04a1acheckpoint(orchestrator): scout-architecture + scout-code-quality findings (2/7)0e6036echeckpoint(orchestrator): scout-security-rules + scout-tests findings (4/7)97090d6checkpoint(orchestrator): scout-docs + scout-lanes-schemas findings (6/7)3bddc08checkpoint(orchestrator): scout phase 7/7 complete; PRD authored; awaiting Joe's "go"Implementation cycle (7)
59dbefbfix(orchestrator): Phase 0 — resolve §7 self-violation; closes q0135aea2bdfeat(phase1): pre-L4b unblockers — D010 + D011 + D012 + frame-coalescing regression test96dc261feat(phase2): test integrity — PoC integration + sub-render coverage + MONTHLY precedence (D013)5744142feat(phase3): CI/compliance integrity — meaningful greens (closes c013)3f9688bfix(phase4): doc-drift sweep — RULES §34 to 2-layer, README layout regenerated, SESSION_01 §G historical stamp (closes q003/q009/q010)b95faa5feat(phase5): operational substrate — logging + status + scheduler + SQLite PRAGMA (closes c011)2118024feat(phase6): data hygiene + EXIT — ProductFile.parse faithful-surface + sub-render contract + retention outlineTest plan
python3 -m pytest tests/ -q→ 117 passed in 0.70s (74 baseline + 43 across the cycle)PYTHONPATH=. python3 scripts/first_hanna_brief.py→ end-to-end pass (state-blind on Linux; FAMILY_LOCKOUT outside Mon–Fri 09–17 ET)python3 -m src status→ emits valid JSON{"hanna": "ok", "harlo_reachable": false, …, "phase": "…", "next_phase_boundary_eta_minutes": …, "ts": "…"}on Linux{coach, patterns, query_past_experience, recall, status}(exactly D001's 5)docs/+ last 50 commit messages → 0 hits after documented exemptionssrc/harlo_bridge.py,src/computations/compute_producer_phase.py,scripts/first_hanna_brief.py); no trailer on fresh seeds (src/_log.py,src/__main__.py,src/schemas.py,state/,.claude/agents/,ORCHESTRATOR.md)grep -rE "claude-opus|Claude Opus 4\." docs/ src/ scripts/ tests/ RULES.md README.md NEXT.md ORCHESTRATOR.md CLAUDE.md HANNA_BLUEPRINT.md→ only CLAUDE.md meta-references to the rule itself)critic[verify]runs PASS on their respective acceptance checklistsBehavior changes that require Joe's attention
These are explicit in the cycle work; flagged here so review catches them:
bin/hanna-brief.commandswap. Was: opensweb/templates/morning_brief.htmlin default browser. Now: runspython3 scripts/first_hanna_brief.pyend-to-end with tee'd logging to~/Library/Logs/hanna-brief.log(macOS) ordata/hanna-brief.log(Linux). Test on your Mac before relying on it for the morning rhythm.bin/com.hanna.brief.morning.plist. Not auto-loaded. To activate: copy to~/Library/LaunchAgents/com.hanna.brief.morning.plistandlaunchctl loadit.bin/README.mddocuments the install. MORNING only as the demonstration; MIDDAY/EVENING/WEEKLY siblings are catalogued for future cycles (q008).ProductFile.parse()is now strict. Duplicate frontmatter keys raiseValueError; unknown## Xheaders log a WARNING and skip the section; empty bullets are dropped; inputs >64KB raise. The four shipped product files (harlo/octavius/moneta/comfy_cozy.md) are clean by inspection; future hand-edited product files need to honor the contract._persistcall.journal_mode=WALmaterializesdata/hanna.sqlite-wal+-shmsidecars alongside the main db (already gitignored via thedata/*.sqlite*glob's effect on the sidecars — verify if you want belt-and-suspenders). The change is fully backward-compatible.src/_log.pyconfigures the root logger on firstget_loggercall. Existing tests don't share state across runs, but if you build other entry points later, the root config is idempotent — subsequentget_loggercalls don't re-configure.Out of scope (deferred — q-IDs)
LockoutResponseshape (high; gates L6mcp_tools)L4b is now dispatchable but not landed in this PR — L4b is its own next-session GOAL through the orchestrator. The contracts it needs (
BriefPayloadrhythm-anchor / idempotency / cross-platform stub) are all here.Reading order
state/gate.md— EXIT receipt for the cycle (Phase results + final belief list + open residual)state/tasks/prd/PRD.md— the PRD that authorized the cycle (7 scout themes + 6-phase plan)ORCHESTRATOR.md— the operating manual (§1–§6 verbatim + §7–§9 Hanna adapter; D009 ratifies)docs/DECISIONS.mdD009 → D013 — the cycle's ratified substrate decisionsstate/beliefs.md+state/open_questions.md— the durable belief + question layerstate/tasks/scout-*/findings.md— the 7 scout reports (BLOCKER/MAJOR/MINOR per area).claude/agents/{planner,worker,critic,integrator}.md— role definitions formalized as Claude Code primitivessrc/schemas.py(D010/D012 helpers + parse hardening),src/_log.py+src/__main__.py(Phase 5),scripts/first_hanna_brief.py(logging integration + PRAGMA + sub-render contract docstring),src/harlo_bridge.py(logging +_log_stderr_tail),bin/(launcher swap + .plist + README)🤖 Generated with Claude Code
Co-Authored-By: Claude [email protected]
Generated by Claude Code
Summary by CodeRabbit
New Features
status) and richer logging output.Tests
Documentation